在我的业余时间玩弄并发,并希望尝试防止撕裂读取而不使用读取器端的锁定,以便并发读取器不会相互干扰。
这个想法是通过锁序列化写入,但在读取端只使用内存屏障。这是一个可重用的抽象,它封装了我提出的方法:
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的理解和应用在上面是否正确?有没有不必要的障碍?
答案 0 :(得分:3)
我仔细研究了你的代码,它看起来对我来说是正确的。立即跳出来的一件事是你使用已建立的模式来执行低锁定操作。我可以看到你使用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位,因此大于我的机器的本机字大小(64位)。我还对作家和读者的数量使用了几种不同的变体。您的低锁技术始终如一地显着优于硬锁技术。我甚至尝试使用Interlocked.CompareExchange
进行保护性读取的一些变体,它们也都慢了。事实上,在某些情况下,它实际上比采取硬锁更慢。我必须诚实。我对此并不感到惊讶。
我还做了一些非常重要的有效性测试。我创建的测试会运行很长一段时间而不是一次我看到一个撕裂的读取。然后作为一个控制测试,我会调整Read
方法,以便我知道这是不正确的,我再次运行测试。正如预期的那样,这一次,撕裂的读数开始随机出现。我把代码切换回你所拥有的东西,撕裂的读数消失了;再次,如预期的那样。这似乎证实了我的预期。也就是说,您的代码看起来正确。我没有各种各样的运行时和硬件环境来测试(我也没有时间)所以我不愿意给它100%的批准印章,但我认为我可以给你的实施两个大拇指现在。
最后,尽管如此,我仍然会避免将其投入生产。是的,这可能是正确的,但是下一个必须维护代码的人可能不会理解它。有人可能会更改代码并打破它,因为他们不了解更改的后果。你必须承认这段代码非常脆弱。即使是最微小的改变也可能破坏它。
答案 1 :(得分:-1)
看起来你对无锁/无等待实现感兴趣。让我们从这个讨论开始,例如: Lock-free multi-threading is for real threading experts