【发布时间】: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 和类似可爱内容的技术。
标签: c# .net multithreading thread-safety thread-synchronization