C#/ CLR:MemoryBarrier和被破坏的读 [英] C#/CLR: MemoryBarrier and torn reads

查看:181
本文介绍了C#/ CLR:MemoryBarrier和被破坏的读的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的闲暇时间,只是和谐地玩,并希望尝试防止撕裂读取,而不使用锁在读者端,因此并发读者不会相互干扰。

Just playing around with concurrency in my spare time, and wanted to try preventing torn reads without using locks on the reader side so concurrent readers don't interfere with each other.

这个想法是通过锁来序列化写入,但在读取端只使用一个内存屏障。这里是一个可重用的抽象,封装了我想出的方法:

The idea is to serialize writes via a lock, but use only a memory barrier on the read side. Here's a reusable abstraction that encapsulate the approach I came up with:

public struct Sync<T>
    where T : struct
{
    object write;
    T value;
    int version; // incremented with each write

    public static Sync<T> Create()
    {
        return new Sync<T> { write = new object() };
    }

    public T Read()
    {
        // if version after read == version before read, no concurrent write
        T x;
        int old;
        do
        {
            // loop until version number is even = no write in progress
            do
            {
                old = version;
                if (0 == (old & 0x01)) break;
                Thread.MemoryBarrier();
            } while (true);
            x = value;
            // barrier ensures read of 'version' avoids cached value
            Thread.MemoryBarrier();
        } while (version != old);
        return x;
    }

    public void Write(T value)
    {
        // locks are full barriers
        lock (write)
        {
            ++version;             // ++version odd: write in progress
            this.value = value;
            // ensure writes complete before last increment
            Thread.MemoryBarrier();
            ++version;             // ++version even: write complete
        }
    }
}

不要担心溢出的版本变量,我避免了另一种方式。那么我的理解和应用Thread.MemoryBarrier是否正确在上面?是否有任何障碍不必要?

Don't worry about overflow on the version variable, I avoid that another way. So is my understanding and application of Thread.MemoryBarrier correct in the above? Are any of the barriers unnecessary?

推荐答案

我花了很长时间仔细看看你的代码,我立即跳出来的一件事是,你使用已建立的模式来执行低锁定操作。我可以看到,你使用版本作为一种虚拟锁。偶数被释放,奇数被获取。由于您对虚拟锁使用单调递增的值,因此您还可以避免 ABA问题。然而,最重要的是,在尝试读取时,您继续循环,直到在读取开始之前观察到虚拟锁定值与相比之后相同 它完成。否则,您认为这是一个失败的读取,并再试一次。所以,在核心逻辑上做得很好。

I took a long hard look at your code and it does appear correct to me. One thing that immediately jumped out at me was that you used an established pattern for performing the low-lock operation. I can see that you are using version as a kind of virtual lock. Even numbers are released and odd numbers are acquired. And since you are using a monotonically increasing value for the virtual lock you are also avoiding the ABA problem. The most important thing, however, is that you continue to loop while attempting to read until the virtual lock value is observed to be the same before the read starts as compared to after it completes. Otherwise, you consider this a failed read and try it all over again. So yeah, job well done on the core logic.

那么内存屏障发生器的位置怎么样呢?好吧,这一切看起来都不错。所有 Thread.MemoryBarrier 调用是必需的。如果我不得不选择,我会说你需要在方法中另外一个,这样看起来像这样。

So what about the placement of the memory barrier generators? Well, this all looks pretty good as well. All of the Thread.MemoryBarrier calls are required. If I had to nit-pick I would say you need one additional one in the Write method so that it looks like this.

public void Write(T value)
{
    // locks are full barriers
    lock (write)
    {
        ++version;             // ++version odd: write in progress
        Thread.MemoryBarrier();
        this.value = value;
        Thread.MemoryBarrier();
        ++version;             // ++version even: write complete
    }
}

调用此处确保 ++版本 this.value = value 不会交换。现在,ECMA规范在技术上允许这种指令重排序。但是,Microsoft的CLI和x86硬件的实现已经在写入时具有volatile语义,因此在大多数情况下并不需要它。

The added call here ensures that ++version and this.value = value do not get swapped. Now, the ECMA specification technically allows that kind of instruction reordering. However, Microsoft's implementation of the CLI and the x86 hardware both already have volatile semantics on writes so it would not really be needed in most cases. But, who knows, maybe it would be necessary on the Mono runtime targeting the ARM cpu.

读取上,可能需要在Mono运行时定位ARM cpu。一边的东西我可以找到没有故障。事实上,你有的呼叫的位置正是我会把它们。有些人可能会想,为什么你不需要一个在版本的初始读之前。原因是因为外部循环将捕获第一次读取缓存的情况,因为 Thread.MemoryBarrier 进一步下降。

On the Read side of things I can find no faults. In fact, the placement of the calls you have is exactly where I would have put them. Some people may wonder why you do not need one before the initial read of version. The reason is because the outer loop will catch the case when the first read was cached because of the Thread.MemoryBarrier further down.

所以这让我讨论性能。这真的比在 Read 方法中采取硬锁定更快吗?嗯,我做了一些相当广泛的测试你的代码,以帮助回答。答案是肯定的!这是相当快一点比采取硬锁。我使用 Guid 作为值类型进行测试,因为它是128位,因此大于我的机器的本机字大小(64位)。我还对作家和读者的数量采用了几种不同的变化。你的低锁技术一贯和显着优于硬锁技术。我甚至尝试了一些变化使用 Interlocked.CompareExchange 做保护的读,他们都慢。事实上,在某些情况下,它实际上比采取硬锁更慢。我要诚实。我对此并不感到惊讶。

So this brings me to a discussion about performance. Is this really faster than taking a hard lock in the Read method? Well, I did some pretty extensive testing of your code to help answer that. The answer is a definitive yes! This is quite a bit faster than taking a hard lock. I tested using a Guid as the value type because it is 128 bits and so that is bigger than the native word size of my machine (64 bits). I also used several different variations on the number of writers and readers. Your low lock technique consistently and significantly outperformed the hard lock technique. I even tried a few variations using Interlocked.CompareExchange to do the guarded read and they were all slower as well. In fact, in some situations it was actually slower than taking the hard lock. I have to be honest. I was not at all surprised by this.

我也做了一些很有意义的有效性测试。我创建的测试会运行一段时间,而不是一次我看到一个撕裂的读。然后作为一个控制测试,我会调整方法,我知道这将是不正确的,我再次运行测试。这一次,如预期,撕裂的读书开始出现随机。我将代码切换回您所拥有的,并且撕掉的读数消失了;再次,如预期。这似乎证实了我已经预期。也就是说,你的代码看起来是正确的。我没有各种各样的运行时和硬件环境来测试(也没有时间),所以我不愿意给它100%的批准,但我认为我可以给你的实现两个大拇指现在。

I also did some pretty significant validity testing. I created tests that would run for quite some time and not once did I see a torn read. And then as a control test I would tweak the Read method in such a manner that I knew it would be incorrect and I ran the test again. This time, as expected, torn reads started to appear randomly. I switched the code back to what you have and the torn reads disappeared; again, as expected. This seemed to confirm what I already expected. That is, your code looks correct. I do not have a wide variety of runtime and hardware environments to test with (nor do I have the time) so I am not willing to give it a 100% seal of approval, but I do think I can give your implementation two thumbs up for now.

最后,尽管如此,我仍然会避免把它放在生产中。是的,这可能是正确的,但下一个人谁必须维护代码可能不会了解它。有人可能改变代码并打破它,因为他们不明白他们的改变的后果。你必须承认这段代码很脆弱。即使是最轻微的变化也会破坏它。

Finally, with all that said, I would still avoid putting this in production. Yeah, it may be correct, but the next guy who has to maintain the code is probably not going to understand it. Someone may change the code and break it because they do not understand the consequences of their changes. You have to admit that this code is pretty brittle. Even the slightest change could break it.

这篇关于C#/ CLR:MemoryBarrier和被破坏的读的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆