naa*_*ing 5 c# clr concurrency memory-barriers concurrent-programming
只是在我的业余时间玩弄并发,并希望尝试防止撕裂读取而不使用读取器端的锁定,因此并发读取器不会相互干扰.
想法是通过锁序列化写入,但在读取端只使用内存屏障.这是一个可重用的抽象,它封装了我提出的方法:
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
}
}
}
Run Code Online (Sandbox Code Playgroud)
不要担心版本变量溢出,我避免另一种方式.那么我对Thread.MemoryBarrier的理解和应用在上面是否正确?有没有不必要的障碍?
我仔细查看了你的代码,它对我来说确实是正确的。我立即注意到的一件事是,您使用了一种既定的模式来执行低锁定操作。我可以看到你正在使用version一种虚拟锁。释放偶数并获取奇数。而且由于您对虚拟锁使用单调递增的值,因此您也避免了ABA 问题。然而,最重要的是,您在尝试读取时继续循环,直到观察到读取开始之前与完成之后的虚拟锁值相同。否则,您会认为这是一次失败的读取并重试。所以,是的,核心逻辑方面做得很好。
那么内存屏障生成器的位置又如何呢?嗯,这一切看起来也不错。所有的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
}
}
Run Code Online (Sandbox Code Playgroud)
此处添加的调用可确保++version且this.value = value不会被交换。现在,ECMA 规范在技术上允许这种指令重新排序。然而,Microsoft 的 CLI 实现和 x86 硬件都已经具有可变的写入语义,因此在大多数情况下实际上并不需要它。但是,谁知道呢,也许在针对 ARM cpu 的 Mono 运行时上有必要。
就Read事情而言,我挑不出任何毛病。事实上,您拨打的电话的位置正是我本来应该放置的位置。有些人可能想知道为什么在初次阅读version. 原因是因为外层循环会捕获第一次读取由于Thread.MemoryBarrier进一步向下而被缓存时的情况。
这让我开始讨论性能。这真的比在方法中采用硬锁更快吗Read?好吧,我对您的代码进行了一些相当广泛的测试来帮助回答这个问题。答案是肯定的!这比采用硬锁要快得多。我测试使用 aGuid作为值类型,因为它是 128 位,因此比我的机器的本机字大小(64 位)大。我还对作者和读者的数量使用了几种不同的变体。您的低锁定技术始终显着优于硬锁定技术。我什至尝试了一些变体Interlocked.CompareExchange来进行保护读取,它们也都比较慢。事实上,在某些情况下,它实际上比采用硬锁要慢。我必须诚实。我对此一点也不感到惊讶。
我还做了一些非常重要的有效性测试。我创建了会运行相当长一段时间的测试,而且我一次都没有看到过破损的读物。然后作为控制测试,我会调整该Read方法,让我知道它是不正确的,然后再次运行测试。这一次,正如所料,撕裂的读物开始随机出现。我把代码改回你所拥有的,撕裂的读数消失了;再次,正如预期的那样。这似乎证实了我已经预料到的事情。也就是说,您的代码看起来是正确的。我没有各种各样的运行时和硬件环境来测试(也没有时间),所以我不愿意给予它 100% 的批准,但我确实认为我可以给你的实现竖起两个大拇指目前。
最后,尽管如此,我仍然会避免将其投入生产。是的,这可能是正确的,但是下一个必须维护代码的人可能不会理解它。有人可能会更改代码并破坏它,因为他们不了解更改的后果。你必须承认这段代码非常脆弱。即使是最轻微的改变也可能会破坏它。
| 归档时间: |
|
| 查看次数: |
350 次 |
| 最近记录: |