gpt4 book ai didi

c# - volatile 读/写和 Thread.MemoryBarrier 排序

转载 作者:太空宇宙 更新时间:2023-11-03 15:07:23 26 4
gpt4 key购买 nike

我正在努力思考内存屏障和 volatile 读/写的微妙之处。我正在阅读 Joseph Albahari 的线程文章:

http://www.albahari.com/threading/part4.aspx

并且在读/写之前何时需要内存屏障以及之后何时需要内存屏障的问题上绊倒了。在“full fences”部分的这段代码中,他在每次写入之后和每次读取之前放置了一个内存屏障:

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

他继续解释:

Barriers 1 and 4 prevent this example from writing “0”. Barriers 2 and 3 provide a freshness guarantee: they ensure that if B ran after A, reading _complete would evaluate to true.

问题 #1: 我对障碍 1 和 4 没有问题,因为它会阻止跨这些障碍重新排序。我不完全理解为什么障碍 2 和 3 是必要的。有人可以解释一下吗,尤其是考虑到如何在 Thread 类中实现 volatile 读取和写入(接下来解释)?

现在我真正开始感到困惑的是,这是 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(正如您在上面看到的那样),则需要将值传递给方法/从方法传递,而不是内联为一个局部变量,因此必须从内存/缓存而不是直接通过寄存器访问,从而消除了在写入之后和读取之前对额外内存屏障的需要。我不知道情况是否如此,但这是我认为它正常工作的唯一方式。

谁能证实或否认是这种情况?

最佳答案

I don't think the memory barriers in the Thread.VolatileRead/Write methods are there to ensure "freshness" of the values at all, but rather to enforce the reordering guarantees.

没错。

Putting the barrier after reads and before writes ensures that no writes will be moved before any reads (but not vice versa).

完整的内存屏障同时具有获取和释放语义,它可以防止先前的内存访问被重新排序到屏障之后以及后续的内存访问被重新排序到屏障之前。

Can anyone confirm or deny that this is the case?

对于在 x86 中 Microsoft 的 .NET 实现中的写入,您可能是正确的,但同样不适用于读取。可以通过 JIT 编译器(可能不使用 no-inlining 属性)或 CPU(即使使用 no-inlining 属性)在先前的访问和内存屏障之间重新排序读取。

但是,这不应改变运行代码看到的内容,尽管读取可能看不到最新鲜值。

int value = 0;
bool done = false;

// in thread 1
value = 123;
Thread.VolatileWrite(ref done, true);

// in thread 2
Thread.SpinUntil(() => Thread.VolatileRead(ref done));
Console.WriteLine(value); // guaranteed 123 due to the memory barrier

在内存模型较弱的其他架构中,写入可以在后续内存访问后重新排序,在极端情况下,其他线程可能直到下一个内存屏障才可见。不过,对于循环,这不是什么大问题。

无论如何,我的建议是不要使用 Thread.VolatileReadThread.VolatileWrite

读取和写入 volatile 字段,Volatile.ReadVolatile.Write 方法提供正确的语义。

尽管 Volatile.ReadVolatile.Write 方法是在 C# 中实现的,就像 Thread.VolatileReadThread 一样。 VolatileWrite,CLR 将方法替换为具有实际 volatile 读/写语义的 native 版本。

关于c# - volatile 读/写和 Thread.MemoryBarrier 排序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42744595/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com