【发布时间】:2017-08-02 08:17:22
【问题描述】:
我正试图围绕内存屏障和易失性读/写的微妙之处展开思考。我在这里阅读 Joseph Albahari 的线程文章:
http://www.albahari.com/threading/part4.aspx
在读/写之前何时需要内存屏障以及之后何时需要内存屏障的问题上磕磕绊绊。在“完整围栏”部分的这段代码中,他在每次写入之后和每次读取之前都放置了一个内存屏障:
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1
_complete = true;
Thread.MemoryBarrier(); // Barrier 2
}
void B()
{
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine (_answer);
}
}
}
他继续解释:
障碍 1 和 4 阻止此示例写入“0”。障碍 2 和 3 提供新鲜度保证:他们确保如果 B 在 A 之后运行, 阅读 _complete 将评估为 true。
问题 #1: 我对障碍 1 和 4 没有意见,因为它会阻止跨这些障碍重新排序。不过,我不完全理解为什么障碍 2 和 3 是必要的。有人可以解释一下,特别是考虑到在Thread 类中如何实现易失性读写(接下来解释)?
现在我真正开始感到困惑的是,这是Thread.VolatileRead/Write() 的实际实现:
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void VolatileWrite (ref int address, int value)
{
MemoryBarrier(); address = value;
}
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static int VolatileRead (ref int address)
{
int num = address; MemoryBarrier(); return num;
}
如您所见,与前面的示例相比,内置的 volatile 函数在每次写入之前(而不是之后)和每次读取之后(而不是之前)都会放置内存屏障。因此,如果我们用基于内置 volatile 函数的等效版本重写前面的示例,它会变成这样:
class Foo
{
int _answer;
bool _complete;
void A()
{
Thread.MemoryBarrier(); // Barrier 1
_answer = 123;
Thread.MemoryBarrier(); // Barrier 2
_complete = true;
}
void B()
{
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 3
Console.WriteLine (_answer);
Thread.MemoryBarrier(); // Barrier 4
}
}
}
问题 #2: 两个 Foo 类在功能上是否相同?为什么或者为什么不?如果需要屏障 2 和 3(在第一个 Foo 类中)来保证写入值并读取实际值,那么 Thread.VolatileXXX 方法不会有点用处吗?
在 StackOverflow 上有几个类似的问题,并得到了公认的答案,例如“屏障 2 确保写入 _complete 不会被缓存”,但它们都没有解决为什么Thread.VolatileWrite() 在这种情况下将内存屏障放在写入之前,以及如果Thread.VolatileRead() 在读取之后放置内存屏障但保证最新值,为什么需要屏障 3。我认为这是最让我失望的地方。
更新:
好的,经过更多的阅读和思考,我有了一个理论,并用我认为可能相关的属性更新了源代码。我认为Thread.VolatileRead/Write 方法中的内存屏障根本不是为了确保值的“新鲜度”,而是为了强制执行重新排序保证。在读取之后和写入之前放置屏障可确保在任何读取之前不会移动任何写入(但反之亦然)。
据我所知,x86 上的所有写入都通过使其他内核上的缓存行无效来保证缓存一致性,因此只要该值未缓存在寄存器中,就可以保证“新鲜度”。我的理论VolatileRead/Write 确保该值不在寄存器中,这可能有点偏离,但我认为我走在正确的轨道上,是他们指望 .NET 实现细节如果它们被标记为MethodImplOptions.NoInlining(如您在上面看到的那样),则需要将值传递给/从方法传递,而不是作为局部变量内联,因此必须从内存/缓存访问而不是直接通过寄存器,从而消除了在写入之后和读取之前额外的内存屏障的需要。我不知道是不是这样,但这是我看到它正常工作的唯一方法。
任何人都可以确认或否认这种情况吗?
【问题讨论】:
-
微软把这件事搞砸了。他们使用 Volatile 类修复它,read the comment。
-
@HansPassant 据我所知,Volatile 方法速度更快,排序保证更少,但 Thread 方法仍然保证新读取等。经过更多挖掘后,我用我的理论更新了我的答案 - 对此有何其他评论?我试图了解易失性读取在读取后如何与内存屏障一起工作。
标签: c# thread-safety memory-barriers