Является ли статический конструктор C# потокобезопасным?

Другими словами, является ли эта реализация Singleton потокобезопасной:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

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

Ответов (10)

Решение

Статические конструкторы гарантированно запускаются только один раз для каждого домена приложения, прежде чем будут созданы какие-либо экземпляры класса или будет осуществлен доступ к статическим членам. https://docs.microsoft.com/en-us/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();
    }
}

Статический конструктор гарантированно потокобезопасен. Также ознакомьтесь с обсуждением Singleton на DeveloperZen: http://web.archive.org/web/20160404231134/http://www.developerzen.com/2007/07/15/whats-wrong-with-this-code -1-обсуждение /

Вот версия Cliffnotes с указанной выше страницы MSDN на синглтоне C#:

Всегда используйте следующий шаблон, вы не ошибетесь:

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

   private Singleton(){}

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

Помимо очевидных функций синглтона, он дает вам эти две вещи бесплатно (относительно синглтона в C++):

  1. ленивая конструкция (или отсутствие конструкции, если она никогда не вызывалась)
  2. синхронизация

Хотя другие ответы в основном верны, есть еще одно предостережение относительно статических конструкторов.

В соответствии с разделом 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();
    }
}

Автор оригинала - Игорь Островский, его пост можно посмотреть здесь .

Статический конструктор завершит работу до того, как какой-либо поток получит доступ к классу.

    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, установленное в нижней части статического конструктора.

Спецификация Common Language Infrastructure гарантирует, что «инициализатор типа должен запускаться ровно один раз для любого заданного типа, если он явно не вызван кодом пользователя». (Раздел 9.5.3.1.) Итак, если у вас нет какой-нибудь дурацкой IL на свободном вызове Singleton ::. Cctor напрямую (маловероятно), ваш статический конструктор будет запускаться ровно один раз перед использованием типа Singleton, будет создан только один экземпляр Singleton, и ваше свойство Instance является потокобезопасным.

Обратите внимание, что если конструктор Singleton обращается к свойству Instance (даже косвенно), тогда свойство Instance будет иметь значение null. Лучшее, что вы можете сделать, - это определить, когда это происходит, и выбросить исключение, проверив, что экземпляр не равен нулю в методе доступа к свойству. После завершения вашего статического конструктора свойство Instance будет отличным от NULL.

Как указывает ответ Zoomba , вам нужно будет сделать Singleton безопасным для доступа из нескольких потоков или реализовать механизм блокировки с использованием экземпляра singleton.

Статические конструкторы гарантированно срабатывают только один раз для каждого домена приложения, поэтому ваш подход должен быть в порядке. Однако функционально он ничем не отличается от более сжатой, встроенной версии:

private static readonly Singleton instance = new Singleton();

Безопасность потоков становится более серьезной проблемой, когда вы лениво инициализируете что-то.

Использование статического конструктора на самом деле является поточно. Статический конструктор гарантированно будет выполнен только один раз.

Из спецификации языка C# :

Статический конструктор класса выполняется не более одного раза в заданном домене приложения. Выполнение статического конструктора запускается первым из следующих событий, которые происходят в домене приложения:

  • Создается экземпляр класса.
  • Ссылка на любой из статических членов класса.

Так что да, вы можете быть уверены, что ваш синглтон будет правильно создан.

Zooba отлично заявила (и за 15 секунд до меня тоже!), Что статический конструктор не гарантирует поточно-безопасный общий доступ к синглтону. С этим нужно будет поступить по-другому.

Хотя все эти ответы дают один и тот же общий ответ, есть одно предостережение.

Помните, что все потенциальные производные от универсального класса компилируются как отдельные типы. Поэтому будьте осторожны при реализации статических конструкторов для универсальных типов.

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

Чтобы быть педантичным, но не существует такой вещи, как статический конструктор, а есть инициализаторы статического типа, вот небольшая демонстрация зависимости циклического статического конструктора, которая иллюстрирует этот момент.