为啥在双重检查锁定中使用 Volatile.Write?
Posted
技术标签:
【中文标题】为啥在双重检查锁定中使用 Volatile.Write?【英文标题】:Why use Volatile.Write in Double-Check Locking?为什么在双重检查锁定中使用 Volatile.Write? 【发布时间】:2021-10-25 13:25:52 【问题描述】:以下是 C# 书籍中的一些代码,展示了如何在多线程中构造单例模式:
internal sealed class Singleton
// s_lock is required for thread safety and having this object assumes that creating
// the singleton object is more expensive than creating a System.Object object
private static readonly Object s_lock = new Object();
// This field will refer to the one Singleton object
private static Singleton s_value = null;
// Private constructor prevents any code outside this class from creating an instance
private Singleton()
// Code to initialize the one Singleton object goes here...
// Public, static method that returns the Singleton object (creating it if necessary)
public static Singleton GetSingleton()
// If the Singleton was already created, just return it (this is fast)
if (s_value != null) return s_value;
Monitor.Enter(s_lock); // Not created, let 1 thread create it
if (s_value == null)
// Still not created, create it
Singleton temp = new Singleton();
// Save the reference in s_value (see discussion for details)
Volatile.Write(ref s_value, temp);
Monitor.Exit(s_lock);
// Return a reference to the one Singleton object
return s_value;
我明白代码为什么会这样:
Singleton temp = new Singleton();
Volatile.Write(ref s_value, temp);
而不是
s_value = new Singleton();
因为编译器可以为Singleton
分配内存,将引用赋值给s_value
,然后调用构造函数。从单个线程的角度来看,像这样更改顺序没有影响。但是如果在将引用发布到s_value
之后,在调用构造函数之前,另一个线程调用了GetSingleton
方法,那么线程会看到s_value
不为空并开始使用Singleton
对象,但它的构造函数有尚未完成执行。
但我不明白为什么我们必须使用Volatile.Write
,我们不能这样做吗:
Singleton temp = new Singleton();
s_value = temp;
编译器不能重新排序,例如先执行s_value = temp
然后执行Singleton temp = new Singleton()
,因为temp
必须在s_value = temp
之前存在?
【问题讨论】:
重点不是要防止new Singleton
行的重新排序(正如你所说,这不可能发生),重点是要防止if (s_value != null)
的重新排序。无论如何它并没有真正的帮助,因为你仍然有一个没有锁的竞争条件,如果你有锁,那么你无论如何都有内存屏障,所以Volatile
不是必需的
在.net中你可以避免它,因为静态构造函数保证以线程安全的方式执行
这里的另一个问题是Monitor.Enter
和Monitor.Exit
应该在try/finally
中,或者更好,就像你应该使用的那样使用lock(
无论你做什么,不要用这本书来指导如何实现单例,因为1)单例一开始是邪恶的,只有在没有更好的创建模式可以解决的情况下才应该考虑事情,2)如果你必须有单例,一个简单的static readonly Singleton = new Singleton()
通常就足够了,由框架保证锁定,3)如果你必须有一个线程安全的, 懒惰初始化的单例,.NET 4 引入了Lazy
,所以没有动力用所有方法来弄错。
提防double-checked locking “该模式在某些语言/硬件组合中实现时可能不安全。有时,它可以被视为反模式。”大多数理智的人会避免使用需要详细了解memory models、cache coherency protocols 和类似可爱内容的技术。
【参考方案1】:
此代码来自 Jeffrey Richter 的 CLR via C#
。
作者在书中的解释(如'see discussion for details'评论中所指)是Volatile.Write
:
确保
temp
中的引用只能发布到s_value
构造函数执行完毕后。
Chris Brumme wrote in 2003 关于相同的 C# 双重检查模式(变量名已更改):
它在 X86 上运行良好。但它会被 ECMA CLI 规范的合法但薄弱的实现所打破。
假设已经发生了一系列商店 在构建 [
Singleton
] 期间。那些店铺可以任意重新排序, 包括将它们推迟到出版之后的可能性 store 将新对象分配给 [s_value
]。那时,有一个 store.release 前的小窗口通过离开锁来暗示。 在该窗口内,其他 CPU 可以浏览参考 [s_value
] 并查看部分构造的实例。
因此,Volatile.Write
仅在为 ECMA CLI standard 内存模型的(理论上)最弱可能实现进行编码时才需要。
注意CLI 标准在这种情况下(12.6.8)要求使用障碍:
明确地不要求一个符合要求的实现 CLI 保证所有状态更新在一个 在构造函数完成之前,构造函数是一致可见的。 CIL 生成器可以通过插入来确保自己的这一要求 适当调用内存屏障或易失性写入 说明。
我无法在 x64 / ARM64 上使用当前版本的 .net 实际演示此问题(由于重新排序而导致部分构造实例的可见性),但我并没有很努力地尝试。
TLDR:写这种代码很复杂,用Lazy<T>
【讨论】:
这很有趣。感谢您参考 ECMA CLI 标准部分以上是关于为啥在双重检查锁定中使用 Volatile.Write?的主要内容,如果未能解决你的问题,请参考以下文章
公司发的小师妹夸我好棒,因为我告诉了她项目中用的双重检查锁定是怎么回事