在没有易失性机制的情况下,CPU 何时写入主存?
Posted
技术标签:
【中文标题】在没有易失性机制的情况下,CPU 何时写入主存?【英文标题】:When does a CPU write to main memory in the absence of a volatile mechanism? 【发布时间】:2020-07-28 11:46:56 【问题描述】:考虑在 x64 或 ARM 上运行的多核/多处理器环境中的以下 C# 代码:
public sealed class Trio
public long A;
public long B;
public long C;
public static class MP
private static readonly object locker = new object();
private static readonly Trio Data = new Trio();
public static Trio ReadCopy()
lock (locker)
return new Trio A = Data.A, B = Data.B, C = Data.C ;
public static void Set(long a, long b, long c)
lock (locker)
Data.A = a;
Data.B = b;
Data.C = c;
线程的同步显然处理得很清楚。
但是,根据我的理解,基于以下观察,我有一个问题:
lock
语句保证 a) 只有一个线程可以访问 Data
和 b) Data
中的字段永远不会被“撕裂”。
Lock
提供了一个内存屏障,据我所知,这在这两种情况下不会有任何明显的影响。
由于字段未标记volatile
,并且由于没有Volatile.Read()
和Volatile.Write()
操作,这三个字段将被写入缓存,而不是直接写入主内存。
直接写入主内存的唯一方法是通过上述“易失性”机制之一,因为这些机制使用ref
操作并禁用优化,从而导致主内存读/写。
查看代码,CPU 会在我不知道的某个时间点将这些字段写入主内存。
我不明白为什么多个线程可以保证看到这三个字段的最新版本,尤其是在 ARM 等弱排序内存架构上。
我的问题是:我如何确定在调用Set()
之后调用ReadCopy()
会看到三个字段的最新值?调用线程可能位于不同的内核上,并且有自己的 Data
缓存副本。
“易变”机制的存在显然是有原因的。该示例通常围绕访问非锁定内存段展开。但是,这里的例子呢?我从未见过使用lock
并且使用易失机制的代码。
【问题讨论】:
您说,“2.Lock
提供了内存屏障...”为什么您认为内存屏障不足以保证ReadCopy()
将返回大多数提供的值最近的Set(...)
电话? (我不是 C# 程序员,但锁/互斥体/无论你怎么称呼它们都可以在我使用过的每一种 其他 编程语言中提供这种保证。)
afana.me/archive/2015/07/10/memory-barriers-in-dot-net.aspx
我不知道内存屏障会将所有未完成的缓存行写入主内存这一事实。 AFAIK,内存屏障与防止重新排序有关,而不是写入主内存。
Java 语言承诺这一点:线程 A 在释放某个锁 L 之前所做的任何分配对于线程 B 在 线程 B 之后都是可见的锁定同一个锁 L。这是一个内存屏障。这限制了硬件对线程 A 和 B 进行的读取和存储重新排序的能力。请注意,没有提到“缓存”和“主内存”。这些词不会出现在 Java 语言的正式规范中任何地方。就像我说的,我不懂 C#,但 C# 是受 Java 启发的。我敢打赌,您无需考虑“缓存”或“主内存”就可以理解 C# 的工作原理。
【参考方案1】:
引自 Igor Ostrovsky 的文章 C# - The C# Memory Model in Theory and Practice:
当一个锁定的代码块执行时,可以保证看到所有来自该块之前的块的写入,按锁定的顺序。此外,保证不会看到任何来自按照锁的顺序跟随它的块的写入。
简而言之,锁隐藏了内存模型的所有不可预测性和复杂性:如果您正确使用锁,您不必担心内存操作的重新排序。
我认为这很彻底地回答了你的问题!
还有第二部分:C# - The C# Memory Model in Theory and Practice, Part 2
【讨论】:
以上是关于在没有易失性机制的情况下,CPU 何时写入主存?的主要内容,如果未能解决你的问题,请参考以下文章