单例的简单实现
本文关键字:实现 简单 单例 | 更新日期: 2023-09-27 17:56:18
实现单例而不是双重检查锁定曼波-jambo的更简单,更安全(因此更好)的方法吗?这种方法有什么缺点吗?
public class Singleton
{
private static Singleton _instance;
private Singleton() { Console.WriteLine("Instance created"); }
public static Singleton Instance
{
get
{
if (_instance == null)
{
Interlocked.CompareExchange(ref _instance, new Singleton(), null);
}
return _instance;
}
}
public void DoStuff() { }
}
编辑:线程安全测试失败,谁能解释为什么?为什么Interlocked.CompareExchange不是真正的原子?
public class Program
{
static void Main(string[] args)
{
Parallel.For(0, 1000000, delegate(int i) { Singleton.Instance.DoStuff(); });
}
}
Result (4 cores, 4 logical processors)
Instance created
Instance created
Instance created
Instance created
Instance created
如果你的单例有多次初始化自己的危险,那么你会遇到更糟糕的问题。为什么不直接使用:
public class Singleton
{
private static Singleton instance=new Singleton();
private Singleton() {}
public static Singleton Instance{get{return instance;}}
}
在初始化方面绝对线程安全。
编辑:如果我不清楚,你的代码是可怕的错误。if
检查和new
都不是线程安全的!您需要使用适当的单例类。
您很可能正在创建多个实例,但这些实例将被垃圾回收,因为它们不在任何地方使用。在任何情况下,静态 _instance 字段变量都不会多次更改其值,即从 null 变为有效值的一次。因此,尽管已经创建了多个实例,但此代码的使用者只会看到相同的实例。
无锁编程
Joe Duffy在他题为《Windows上的并发编程》的书中,实际上分析了你试图在第10章"内存模型和锁定自由"第526页中使用的这种模式。
他将此模式称为宽松引用的延迟初始化:
public class LazyInitRelaxedRef<T> where T : class
{
private volatile T m_value;
private Func<T> m_factory;
public LazyInitRelaxedRef(Func<T> factory) { m_factory = factory; }
public T Value
{
get
{
if (m_value == null)
Interlocked.CompareExchange(ref m_value, m_factory(), null);
return m_value;
}
}
/// <summary>
/// An alternative version of the above Value accessor that disposes
/// of garbage if it loses the race to publish a new value. (Page 527.)
/// </summary>
public T ValueWithDisposalOfGarbage
{
get
{
if (m_value == null)
{
T obj = m_factory();
if (Interlocked.CompareExchange(ref m_value, obj, null) != null && obj is IDisposable)
((IDisposable)obj).Dispose();
}
return m_value;
}
}
}
正如我们所看到的,在上面的示例中,方法以创建丢弃对象为代价是无锁定的。在任何情况下,对于此类 API 的使用者,Value 属性都不会更改。
平衡权衡取舍
Lock Freedom是有代价的,并且是仔细选择权衡的问题。在这种情况下,锁定自由的代价是您必须创建不打算使用的对象的实例。这可能是一个可以接受的代价,因为您知道通过无锁,死锁和线程争用的风险更低。
然而,在这种特殊情况下,单例的语义本质上是创建对象的单个实例,所以我宁愿选择Lazy<T>
,正如@Centro在他的回答中引用的那样。
尽管如此,它仍然引出了一个问题,我们什么时候应该使用Interlocked.CompareExchange
?我喜欢你的例子,它发人深省,当它不像引号那样严重错误时,许多人很快就会将其diss为错误@Blindy。
这一切都归结为您是否计算了权衡并决定:
- 生成
- 一个且仅生成一个实例有多重要?
- 无锁有多重要?
只要您意识到权衡,并有意识地决定创建新对象以利于无锁,那么您的示例也可能是一个可以接受的答案。
为了不使用"双重检查锁定 mambo-jambo",或者干脆不实现自己的单例重新发明轮子,请使用 .NET 4.0 中包含的现成解决方案 - Lazy
public class Singleton
{
private static Singleton _instance = new Singleton();
private Singleton() {}
public static Singleton Instance
{
get
{
return _instance;
}
}
}
相信你可以完全相信这一点。 是的,Interlocked.CompareExchanger 是原子的,但新的 Singleton() 在任何非平凡的情况下都不会是原子的。 由于它必须在交换值之前进行评估,因此这通常不是线程安全的实现。
这个呢?
public sealed class Singleton
{
Singleton()
{
}
public static Singleton Instance
{
get
{
return Nested.instance;
}
}
class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}
internal static readonly Singleton instance = new Singleton();
}
}
这是此页面上的第五个版本:http://www.yoda.arachsys.com/csharp/singleton.html
我不确定,但作者似乎认为它既线程安全又延迟加载。
您的单一实例初始值设定项的行为完全符合预期。参见 Raymond Chen 的无锁算法: 单例构造函数:
这是一个双重检查锁,但没有锁定。在进行初始构造时,我们没有锁定,而是让它成为谁可以创建对象的自由竞争。如果五个线程同时访问此代码,当然,让我们创建五个对象。在每个人都创建了他们认为是获胜的对象之后,他们调用InterlockedCompareExchangePointerRelease来尝试更新全局指针。
当可以让多个线程尝试创建单例(并让所有失败者销毁其副本)时,此技术适用。如果创建单例成本高昂或有不必要的副作用,那么您不想使用免费算法。
每个线程都会创建对象;因为它认为还没有人创建它。但是在InterlockedCompareExchange
期间,只有一个线程能够真正设置全局单例。
奖金阅读
- 一次性初始化帮助程序函数使您不必自己编写所有这些代码。它们处理所有同步和内存屏障问题,并支持单人初始化和免费初始化模型。
- .NET 的延迟初始化原语提供了相同的 C# 版本。
这不是线程安全的。
您需要一把锁来将if()
和Interlocked.CompareExchange()
固定在一起,然后您就不再需要CompareExchange
了。
您仍然遇到一个问题,即很可能正在创建和丢弃单例实例。执行 Interlocked.CompareExchange()
时,无论赋值是否成功,都将始终执行 Singleton
构造函数。所以你不会比你说更好(或更糟,恕我直言):
if ( _instance == null )
{
lock(latch)
{
_instance = new Singleton() ;
}
}
与将lock
的位置和测试交换为 null 相比,相对于线程争用的性能更好,但有构造额外实例的风险。
.NET 的明显单例实现?
自动属性初始化 (C# 6.0) 似乎不会导致您看到的 Singleton 的多个实例化。
public class Singleton
{
static public Singleton Instance { get; } = new Singleton();
private Singleton();
}
我认为.NET 4.0
之后最简单的方法是使用 System.Lazy<T>
:
public class Singleton
{
private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance { get { return lazy.Value; } }
private Singleton() { }
}
Jon Skeet在这里有一篇不错的文章,涵盖了实现单例的许多方法以及每种方法的问题。
不要使用锁定。使用您的语言环境
大多数简单的线程安全实现是:
public class Singleton
{
private static readonly Singleton _instance;
private Singleton() { }
static Singleton()
{
_instance = new Singleton();
}
public static Singleton Instance
{
get { return _instance; }
}
}