.NET 内存模型、易失性变量以及测试和设置:有什么保证?
-
21-09-2019 - |
题
我知道 .NET 内存模型(在 .NET Framework 上;not Compact/micro/silverlight/mono/xna/what-have-you)保证对于某些类型(最显着的是原始整数和引用)操作保证是原子的。
此外,我相信 x86/x64 测试和设置指令(以及 Interlocked.CompareExchange
)实际上引用了全局内存位置,所以如果它成功另一个 Interlocked.CompareExchange
会看到新的值。
最后,我相信, volatile
关键字是一个指令 编译器 尽快传播读取和写入,并且不重新排序与此变量相关的操作(对吗?)。
这引出了几个问题:
- 我的上述信念正确吗?
Interlocked.Read
没有 int 的重载,只有 long 的重载(它们是 2 个字,因此通常不会以原子方式读取)。我总是假设 .NET 内存模型保证在读取整数/引用时看到最新的值,但是对于处理器缓存、寄存器等。我开始发现这可能是不可能的。那么有没有办法强制重新获取变量呢?- 对于整数和引用来说,挥发性足以解决上述问题吗?
- 在 x86/x64 上我可以假设...
如果有两个全局整型变量 x 和 y,都初始化为 0,如果我写:
x = 1;
y = 2;
没有线程会看到 x = 0 和 y = 2 (即写入将按顺序发生)。如果它们不稳定,这会改变吗?
解决方案
- 只有对最多 32 位宽(在 x64 系统上为 64 位宽)的变量的读取和写入才是原子的。所有这一切都意味着你不会阅读 整数 并得到半写值。这并不意味着算术是原子的。
- 互锁操作也充当内存屏障,所以是的,
Interlocked.CompareExchange
将看到更新后的值。 - 看 这一页. 。易变并不意味着有序。一些编译器可能会选择不对易失性变量的操作重新排序,但 CPU 可以自由地重新排序。如果要阻止 CPU 对指令重新排序,请使用(完整)内存屏障。
- 内存模型确保读取和写入是原子的,并且使用 volatile 关键字确保读取将 总是 来自内存,而不是来自寄存器。那么你 将要 查看最新值。这是因为 x86 CPU 会在适当的时候使缓存失效 - 请参阅 这 和 这. 。另请参阅 InterlockedCompareExchange64 了解如何自动读取 64 位值。
- 最后,最后一个问题。答案是一个线程实际上可以看到
x = 0
和y = 2
, ,并且使用 volatile 关键字不会改变这一点,因为 CPU 可以自由地重新排序指令。你需要一个记忆屏障。
概括:
- 编译器可以自由地重新排序指令。
- CPU 可以自由地重新排序指令。
- 字大小的读取和写入是原子的。算术和其他操作不是原子的,因为它们涉及读取、计算,然后写入。
- 从内存中读取字大小的数据将始终检索最新值。但大多数时候你不知道自己是否真的在凭记忆阅读。
- 完整的内存屏障会停止 (1) 和 (2)。大多数编译器允许您自行停止 (1)。
- 易失性关键字确保您从内存中读取 - (4)。
- 互锁操作(锁前缀)允许多个操作是原子的。例如,读+写(InterlockedExchange)。或者读+比较+写(InterlockedCompareExchange)。它们还充当内存屏障,因此(1)和(2)被停止。它们总是写入内存(显然),因此(4)是可以保证的。
其他提示
遇到了这个旧线程。Hans 和 wj32 的答案都是正确的,除了关于 volatile
.
具体针对你的问题
在 x86/x64 上我可以假设...如果 有两个全局整数变量 x 和 y,两者都初始化为 0,如果 我写:
x = 1; y = 2;
没有线程会看到 x = 0 和 y = 2(即写入将 按顺序发生)。如果出现以下情况,这是否会改变 它们是否易变?
如果 y
是易失性的,写入 x
保证在写入之前发生 y
, ,因此没有线程会看到 x = 0
和 y = 2
. 。这是因为对易失性变量的写入具有“释放语义”(逻辑上相当于释放栅栏的发射),即在它不会移动之前的所有读/写指令都会通过它。(这意味着如果 x 是不稳定的,但 y 不是,您可能仍然会看到意外的情况 x = 0
和 y = 2
.) 请参阅中的描述和代码示例 C# 规范 更多细节。
不,易失性关键字和原子性保证太弱了。你需要一个内存屏障来确保这一点。您可以使用 Thread.MemoryBarrier() 方法显式获取一个。