C#静态构造函数线程安全吗?


247

换句话说,此Singleton实现线程是否安全:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

    static Singleton()
    {
        instance = new Singleton();
    }

    public static Singleton Instance
    {
        get { return instance; }
    }
}

1
这是线程安全的。假设有多个线程想要Instance一次获取该属性。将告知其中一个线程首先运行类型初始化器(也称为静态构造函数)。同时,所有其他想要读取该Instance属性的线程将被锁定,直到类型初始化程序完成为止。只有在字段初始化程序结束后,才允许线程获取Instance值。所以没有人能看到Instance存在null
Jeppe Stig Nielsen

@JeppeStigNielsen其他线程未锁定。根据我自己的经验,我因此而遇到了令人讨厌的错误。可以保证只有第一个线程会启动静态初始化程序或构造函数,但是其他线程将尝试使用静态方法,即使构造过程尚未完成。
Narvalex

2
@Narvalex 此示例程序(使用URL编码的源代码)无法重现您描述的问题。也许这取决于您使用的CLR版本?
杰普·斯蒂格·尼尔森

@JeppeStigNielsen感谢您抽出宝贵的时间。您能告诉我为什么此字段被覆盖吗?
Narvalex

5
@Narvalex与该代码,大写X最终被-1 即使没有螺纹。这不是线程安全问题。相反,初始化程序x = -1首先运行(它在代码的较早一行,较低的行号)。然后X = GetX()运行初始化程序,使大写X等于-1。然后,运行“显式”静态构造函数,即类型初始化器static C() { ... }运行,它只会更改小写字母x。因此,毕竟,该Main方法(或Other方法)可以继续并读取大写字母X-1即使只有一个线程,其值也将是。
杰普·斯蒂格·尼尔森

Answers:


189

在创建类的任何实例或访问任何静态成员之前,保证静态构造函数在每个应用程序域仅运行一次。 https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

所示的实现对于初始构造是线程安全的,也就是说,构造Singleton对象不需要任何锁定或null测试。但是,这并不意味着实例的任何使用都会被同步。有多种方法可以完成此操作。我在下面显示了一个。

public class Singleton
{
    private static Singleton instance;
    // Added a static mutex for synchronising use of instance.
    private static System.Threading.Mutex mutex;
    private Singleton() { }
    static Singleton()
    {
        instance = new Singleton();
        mutex = new System.Threading.Mutex();
    }

    public static Singleton Acquire()
    {
        mutex.WaitOne();
        return instance;
    }

    // Each call to Acquire() requires a call to Release()
    public static void Release()
    {
        mutex.ReleaseMutex();
    }
}

53
请注意,如果您的单例对象是不可变的,则使用互斥锁或任何同步机制是过大的,不应使用。另外,我发现上述示例实现非常脆弱:-)。使用Singleton.Acquire()的所有代码在使用singleton实例完成后,都将调用Singleton.Release()。如果不这样做(例如过早返回,通过异常离开作用域,忘记调用Release),则下次从另一个线程访问此Singleton时,它将死于Singleton.Acquire()中。
米兰·加迪安

2
同意,尽管我会走得更远。如果您的单身人士是一成不变的,那么使用单身人士就太过分了。只需定义常量即可。最终,正确使用单例要求开发人员知道他们在做什么。尽管此实现非常脆弱,但它仍然比问题中那些错误随机显示而不是明显不发布的互斥锁更好。
Zooba 2009年

26
减轻Release()方法的脆弱性的一种方法是使用另一个具有IDisposable的类作为同步处理程序。当您获取单例时,您将获得处理程序,并将需要单例的代码放入using块中以处理释放。
CodexArcanum 2010年

5
对于其他可能为此而烦恼的人:调用静态构造函数之前,将初始化具有初始化程序的所有静态字段成员。
亚当·麦金莱

12
这些天的答案是使用Lazy<T>-使用我最初发布的代码的任何人都做错了(老实说,开始不是那么好-5年前我在这方面不如当前-我是 :) )。
Zooba 2014年

86

尽管所有这些答案都给出了相同的一般性答案,但有一个警告。

请记住,泛型类的所有潜在派生类都被编译为单个类型。因此,在为泛型类型实现静态构造函数时要格外小心。

class MyObject<T>
{
    static MyObject() 
    {
       //this code will get executed for each T.
    }
}

编辑:

这是演示:

static void Main(string[] args)
{
    var obj = new Foo<object>();
    var obj2 = new Foo<string>();
}

public class Foo<T>
{
    static Foo()
    {
         System.Diagnostics.Debug.WriteLine(String.Format("Hit {0}", typeof(T).ToString()));        
    }
}

在控制台中:

Hit System.Object
Hit System.String

typeof(MyObject <T>)!= typeof(MyObject <Y>);
卡里姆·阿哈

6
我想这就是我要提出的重点。泛型类型根据使用泛型参数而编译为单个类型,因此静态构造函数可以并且将被多次调用。
Brian Rudolph

1
当T为值类型时,这是正确的,对于引用类型T,将仅生成一种泛型类型
sll

2
@sll:不是真的...查看我的编辑
布赖恩·鲁道夫

2
有趣但真正静态的cosntructor要求所有类型,只是尝试了多种引用类型
sll

28

实际上,使用静态构造函数线程安全的。保证静态构造函数只能执行一次。

根据C#语言规范

类的静态构造函数在给定的应用程序域中最多执行一次。静态构造函数的执行由以下在应用程序域中发生的事件中的第一个触发:

  • 创建该类的实例。
  • 引用了该类的任何静态成员。

因此,是的,您可以相信您的单例将被正确实例化。

Zooba指出了一个很好的观点(也是在我面前15秒钟!),静态构造函数将不能保证对单例的线程安全共享访问。这将需要以另一种方式来处理。


8

这是上面C#单例上的MSDN页面的Cliffnotes版本:

始终使用以下模式,您不会出错:

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();

   private Singleton(){}

   public static Singleton Instance
   {
      get 
      {
         return instance; 
      }
   }
}

除了明显的单例功能之外,它还为您免费提供了这两件事(相对于c ++中的单例):

  1. 惰性构造(如果从未调用过,则不构造)
  2. 同步化

3
如果该类没有任何其他不相关的静态变量(例如consts),则可以延迟。否则,访问任何静态方法或属性都将导致实例创建。所以我不会懒惰。
Schultz9999

6

保证静态构造函数在每个App Domain中仅触发一次,因此您的方法应该可以。但是,它在功能上与更简洁的嵌入式版本没有什么不同:

private static readonly Singleton instance = new Singleton();

懒惰地初始化事物时,线程安全性更成问题。


4
安德鲁,那并不完全等同。通过不使用静态构造函数,有关何时执行初始化程序的某些保证将丢失。请查看以下链接以获取详细说明:* < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

Derek,我熟悉beforefieldinit的 “优化”,但就我个人而言,我从不担心。
安德鲁·彼得斯


4

静态构造函数将允许任何线程访问该类之前完成运行。

    private class InitializerTest
    {
        static private int _x;
        static public string Status()
        {
            return "_x = " + _x;
        }
        static InitializerTest()
        {
            System.Diagnostics.Debug.WriteLine("InitializerTest() starting.");
            _x = 1;
            Thread.Sleep(3000);
            _x = 2;
            System.Diagnostics.Debug.WriteLine("InitializerTest() finished.");
        }
    }

    private void ClassInitializerInThread()
    {
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() starting.");
        string status = InitializerTest.Status();
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() status = " + status);
    }

    private void classInitializerButton_Click(object sender, EventArgs e)
    {
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
    }

上面的代码产生了下面的结果。

10: ClassInitializerInThread() starting.
11: ClassInitializerInThread() starting.
12: ClassInitializerInThread() starting.
InitializerTest() starting.
InitializerTest() finished.
11: ClassInitializerInThread() status = _x = 2
The thread 0x2650 has exited with code 0 (0x0).
10: ClassInitializerInThread() status = _x = 2
The thread 0x1f50 has exited with code 0 (0x0).
12: ClassInitializerInThread() status = _x = 2
The thread 0x73c has exited with code 0 (0x0).

即使静态构造函数花很长时间运行,其他线程也会停止并等待。所有线程都读取静态构造函数底部的_x值。


3

共同语言基础设施规格保证“一类型初始不得以任何给定类型刚好运行一次,除非用户代码显式调用。” (第9.5.3.1节)因此,除非您直接(不太可能)在松散地调用Singleton ::。cctor上有一些怪异的IL,否则您的静态构造函数在使用Singleton类型之前将只运行一次,因此只会创建一个Singleton实例,并且您的Instance属性是线程安全的。

请注意,如果Singleton的构造函数访问Instance属性(甚至是间接访问),则Instance属性将为null。最好的办法是通过检查实例在属性访问器中是否为非空来检测何时发生这种情况并引发异常。静态构造函数完成后,Instance属性将为非null。

正如Zoomba的答案所指出的那样,您将需要使Singleton安全以从多个线程进行访问,或者在使用Singleton实例周围实现锁定机制。




0

尽管其他答案大多数都是正确的,但静态构造函数还有另一个警告。

由于每节II.10.5.3.3竞争和死锁的的ECMA-335通用语言基础

除非从类型初始值设定项调用的某些代码(直接或间接)显式调用阻塞操作,否则仅类型初始化不会产生死锁。

以下代码导致死锁

using System.Threading;
class MyClass
{
    static void Main() { /* Won’t run... the static constructor deadlocks */  }

    static MyClass()
    {
        Thread thread = new Thread(arg => { });
        thread.Start();
        thread.Join();
    }
}

原始作者是Igor Ostrovsky,请在此处查看他的帖子。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.