【问题标题】:Is the C# static constructor thread safe?C# 静态构造函数线程安全吗?
【发布时间】:2010-09-05 15:29:52
【问题描述】:

换句话说,这个 Singleton 实现线程安全吗:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

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

【问题讨论】:

  • 它是线程安全的。假设多个线程想要一次获取属性Instance。其中一个线程将被告知首先运行类型初始化程序(也称为静态构造函数)。同时,所有其他想要读取Instance 属性的线程将被锁定,直到类型初始化程序完成。只有在字段初始化程序结束后,才会允许线程获取Instance 值。所以没有人可以看到Instancenull
  • @JeppeStigNielsen 其他线程未锁定。根据我自己的经验,我因此遇到了令人讨厌的错误。保证只有第一个线程会启动静态初始化器或构造器,但随后其他线程将尝试使用静态方法,即使构造过程没有完成。
  • @Narvalex This sample program(在 URL 中编码的源代码)无法重现您描述的问题。也许这取决于您拥有的 CLR 版本?
  • @JeppeStigNielsen 感谢您抽出宝贵时间。你能解释一下为什么here这个字段被覆盖了吗?
  • @Narvalex 使用该代码,大写的X 最终变成-1 即使没有线程。这不是线程安全问题。相反,初始化程序 x = -1 首先运行(它位于代码的较早行,较低的行号)。然后初始化器X = GetX() 运行,这使得大写的X 等于-1。然后是“显式”静态构造函数,类型初始化器static C() { ... } 运行,它只更改小写x。所以毕竟,Main 方法(或Other 方法)可以继续读取大写的X。它的值将是-1,即使只有一个线程。

标签: c# multithreading singleton


【解决方案1】:

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

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

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();
    }
}

【讨论】:

  • 请注意,如果您的单例对象是不可变的,则使用互斥锁或任何同步机制都是过大的,不应使用。另外,我发现上面的示例实现非常脆弱:-)。所有使用 Singleton.Acquire() 的代码在使用单例实例完成后都应调用 Singleton.Release()。如果不这样做(例如过早返回、通过异常离开作用域、忘记调用 Release),下次从其他线程访问此 Singleton 时,它将在 Singleton.Acquire() 中死锁。
  • 同意,但我会更进一步。如果你的单例是不可变的,那么使用单例就太过分了。只需定义常量。最终,正确使用单例需要开发人员知道他们在做什么。尽管这个实现很脆弱,但它仍然比问题中那些错误随机出现而不是明显未发布的互斥体更好。
  • 减少 Release() 方法脆弱性的一种方法是使用另一个具有 IDisposable 的类作为同步处理程序。当你获取单例时,你会得到处理程序,并且可以将需要单例的代码放入 using 块中来处理释放。
  • 对于其他可能被此绊倒的人:任何带有初始化器的静态字段成员都在调用静态构造函数之前进行初始化。
  • 这些天的答案是使用Lazy<T> - 任何使用我最初发布的代码的人都做错了(老实说,从 5 年前开始并不是那么好 -我在这方面不如现在的我好:))。
【解决方案2】:

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

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

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) != typeof(MyObject);
  • 我认为这就是我想要表达的观点。泛型类型根据使用的泛型参数编译为单独的类型,因此静态构造函数可以并且将被多次调用。
  • 当 T 是值类型时这是对的,对于引用类型 T 只会生成一个泛型类型
  • @sll: Not True... 查看我的编辑
  • 有趣但真正静态的 cosntructor 调用了所有类型,只是尝试了多种引用类型
【解决方案3】:

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

From the C# language specification:

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

  • 创建了一个类的实例。
  • 类的任何静态成员都被引用。

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

Zooba 提出了一个很好的观点(也比我早了 15 秒!)静态构造函数不能保证对单例的线程安全共享访问。这将需要以其他方式处理。

【讨论】:

    【解决方案4】:

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

    使用以下模式,永远不会出错:

    public sealed class Singleton
    {
       private static readonly Singleton instance = new Singleton();
    
       private Singleton(){}
    
       public static Singleton Instance
       {
          get 
          {
             return instance; 
          }
       }
    }
    

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

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

    【讨论】:

    • 如果类没有任何其他不相关的静态变量(如 consts),则为惰性。否则访问任何静态方法或属性将导致实例创建。所以我不会称之为懒惰。
    【解决方案5】:

    静态构造函数保证每个 App Domain 只触发一次,所以你的方法应该没问题。但是,它在功能上与更简洁的内联版本没有什么不同:

    private static readonly Singleton instance = new Singleton();
    

    当您懒惰地初始化事物时,线程安全是一个更大的问题。

    【讨论】:

    • 安德鲁,这并不完全等同。由于不使用静态构造函数,一些关于何时执行初始化程序的保证会丢失。请查看这些链接以获得深入的解释:* csharpindepth.com/Articles/General/Beforefieldinit.aspx> * ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html>
    • Derek,我对 beforefieldinit “优化”很熟悉,但就我个人而言,我从不担心。
    【解决方案6】:

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

        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的值。

    【讨论】:

    • 那么静态构造函数在运行时会阻塞所有其他线程吗?如果您有一个非常慢的静态构造函数并且它在应用程序启动后运行较晚(例如,即使从线程池运行它也会阻塞 UI 线程),这听起来会导致引入问题。
    【解决方案7】:

    Common Language Infrastructure specification 保证“除非用户代码显式调用,否则类型初始化程序应为任何给定类型只运行一次。” (第 9.5.3.1 节。)因此,除非您在松散调用 Singleton::.cctor 上有一些古怪的 IL(不太可能),否则您的静态构造函数将在使用 Singleton 类型之前只运行一次,只会创建一个 Singleton 实例,并且您的 Instance 属性是线程安全的。

    请注意,如果 Singleton 的构造函数访问 Instance 属性(即使是间接访问),则 Instance 属性将为 null。您可以做的最好的事情是检测何时发生这种情况并抛出异常,方法是检查属性访问器中的实例是否为非空。在您的静态构造函数完成后,Instance 属性将是非空的。

    正如Zoomba's answer 指出的那样,您需要使单例可以安全地从多个线程访问,或者围绕使用单例实例实施锁定机制。

    【讨论】:

      【解决方案8】:

      只是为了迂腐,但没有静态构造函数之类的东西,而是静态类型初始化器,here's a small 循环静态构造函数依赖的演示说明了这一点。

      【讨论】:

      【解决方案9】:

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

      根据ECMA-335 Common Language Infrastructure

      II.10.5.3.3 竞赛和死锁部分

      类型初始化本身不会造成死锁,除非某些代码 从类型初始化器(直接或间接)显式调用 调用阻塞操作。

      以下代码导致死锁

      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,见他的帖子here

      【讨论】:

        【解决方案10】:

        静态构造函数保证是线程安全的。 另外,请查看 DeveloperZen 上关于 Singleton 的讨论: http://web.archive.org/web/20160404231134/http://www.developerzen.com/2007/07/15/whats-wrong-with-this-code-1-discussion/

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多