内存屏障如何实现线程安全?示例代码中隐式内存屏障位置在哪?
内存屏障与线程安全的疑问解答
原文引用
《C# 10 in a Nutshell》作者Joseph Albahari提到:
内存屏障是一道“栅栏”,重排序和缓存的影响无法穿透它。
这不仅适用于锁,还适用于所有同步构造。因此,如果你使用的信号构造能确保同一时间只有一个线程读写变量,就不需要加锁。比如以下代码在不对x加锁的情况下也是线程安全的:
示例代码
var signal = new ManualResetEvent (false); int x = 0; new Thread (() => { x++; signal.Set(); }).Start(); signal.WaitOne(); Console.WriteLine (x); // 1 (always)
要注意的是,x++本身并非线程安全的操作。有用户据此认为,上述代码里的x++是通过隐式执行以下逻辑实现线程安全的:
Thread.MemoryBarrier(); x++; Thread.MemoryBarrier();
问题解答
1. 内存屏障仅通过阻止重排序和缓存,如何真正实现线程安全?
得先搞清楚:内存屏障本身不能直接让非原子操作(比如x++)变成线程安全。它管的是内存可见性和指令重排序问题,而这个示例里线程安全的核心,是ManualResetEvent的同步逻辑保证了「同一时间只有一个线程操作x」:
- 子线程必须先执行完
x++,才会调用signal.Set();主线程则必须等signal.WaitOne()返回后,才会去读x。这就彻底避免了多线程同时读写x的情况。
内存屏障在这里的作用是保驾护航:
- 防止CPU或编译器把
x++和signal.Set()的执行顺序搞反,保证x的修改先写入内存,再触发信号; - 保证主线程在
signal.WaitOne()之后读到的x是内存里的最新值,而不是CPU缓存里的旧数据。
要是没有内存屏障,可能出现信号先触发但x还没更新,或者主线程读到旧值的情况,但内存屏障解决了这些问题,再配合信号量的同步逻辑,最终实现了线程安全。
2. 在上述示例代码中,所有隐式MemoryBarrier()具体会被放置在哪些位置?
在.NET里,ManualResetEvent的Set()和WaitOne()方法内部都会自动插入内存屏障,具体位置是:
- 子线程中:
x++执行完成后、signal.Set()调用前,会插入写屏障;同时signal.Set()内部也包含内存屏障逻辑,确保x的修改对其他线程可见。 - 主线程中:
signal.WaitOne()返回后、Console.WriteLine(x)执行前,会插入读屏障,保证后续读取x时直接从内存取最新值,不用缓存里的旧数据。
你完全不用手动调用Thread.MemoryBarrier(),这些同步构造自带的内存屏障,就是为了保证同步前后的内存操作顺序和可见性。
内容的提问来源于stack exchange,提问作者Ivan Petrov
相关产品推荐
相关产品推荐

