只是在业余时间使用并发性,并且想尝试在不使用读取器端锁定的情况下防止读取中断,从而使并发读取器不会相互干扰。

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

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的理解和应用在上面是否正确?是否没有任何障碍?

最佳答案

我仔细检查了一下您的代码,它对我来说似乎是正确的。立刻引起我注意的一件事是,您使用了已建立的模式来执行低锁操作。我可以看到您正在使用version作为一种虚拟锁。偶数被释放,而奇数被获取。并且由于您为虚拟锁使用单调递增的值,因此您也避免了ABA problem。但是,最重要的是,您在尝试读取时继续循环,直到观察到虚拟锁值在读取开始之前与读取完成之后相同为止。否则,您认为这是一次失败的阅读,然后再试一次。是的,在核心逻辑上做得很好。

那么如何设置内存屏障生成器呢?好吧,这一切看起来也不错。所有Thread.MemoryBarrier调用都是必需的。如果我不得不挑剔,我会说你需要在Write方法中再增加一个,以便它看起来像这样。

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
    }
}

此处添加的调用可确保++versionthis.value = value不被交换。现在,ECMA规范从技术上允许这种指令重新排序。但是,Microsoft的CLI和x86硬件实现都已经在写入时具有易变的语义,因此在大多数情况下实际上并不需要它。但是,谁知道,也许在针对ARM cpu的Mono运行时中是必要的。

Read方面,我找不到任何错误。实际上,您所拥有的电话的放置位置恰好是我放置它们的位置。有些人可能想知道为什么在初次阅读version之前不需要一个。原因是因为Thread.MemoryBarrier进一步下降,外循环将捕获第一次读取时的情况。

因此,这使我进入了有关性能的讨论。这真的比硬锁Read方法快吗?好吧,我对您的代码做了一些相当广泛的测试,以帮助回答这一问题。答案是肯定的!这比硬锁快很多。我使用Guid作为值类型进行了测试,因为它是128位,因此比我的机器的 native 字长(64位)大。我还对作者和读者的数量使用了几种不同的变体。您的低锁定技术始终优于硬锁定技术。我什至尝试使用Interlocked.CompareExchange进行了一些变体来进行 protected 读取,但它们的速度也都较慢。实际上,在某些情况下,它实际上比采取硬锁要慢。我要说实话我对此一点都不感到惊讶。

我还做了一些相当重要的有效性测试。我创建了可以运行相当长一段时间的测试,而没有一次我看到阅读破烂的消息。然后,作为对照测试,我将对Read方法进行调整,以使我知道它是不正确的,然后再次运行该测试。如预期的那样,这次撕裂的读取开始随机出现。我将代码切换回您已有的代码,并且撕裂的读数消失了;再次如预期。这似乎证实了我已经期望的结果。也就是说,您的代码看起来正确。我没有各种各样的运行时和硬件环境可以进行测试(也没有时间),因此我不愿意给予它100%的认可,但是我确实可以给您的实现两个建议目前。

最后,尽管如此,我仍然会避免将其投入生产。是的,这可能是正确的,但是下一个必须维护代码的人可能不会理解它。有人可能会更改代码并破坏代码,因为他们不了解更改的后果。您必须承认此代码非常脆弱。即使是最细微的变化也可能破坏它。

关于C#/CLR : MemoryBarrier and torn reads,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/18935939/

10-10 01:03