只是在业余时间使用并发性,并且想尝试在不使用读取器端锁定的情况下防止读取中断,从而使并发读取器不会相互干扰。
这个想法是通过锁序列化写操作,但是在读端只使用一个内存屏障。这是一个可重用的抽象,它封装了我想出的方法:
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
}
}
此处添加的调用可确保
++version
和this.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/