Каким образом реализация спецификации c sharp обеспечивает выполнение статических конструкторов безопасным для потоков способом? - PullRequest
0 голосов
/ 27 октября 2018

Статические конструкторы c # гарантированно будут выполняться только один раз.Поэтому, если я скажу, что десять потоков обращаются к члену класса A, а статический конструктор A не был запущен, а статическому конструктору A требуется 10 секунд для запуска, эти потоки будут блокироваться на десять секунд.

Это кажется мне поразительным - как это достигается в JIT / CLR?Каждый ли доступ к статическому полю входит в блокировку, проверяет, инициализирован ли статический конструктор, а затем инициализирует его, если это не так?Разве это не будет очень медленно?

Чтобы было ясно, я хочу знать, как реализация спецификации достигает этого.Я знаю, что статические конструкторы являются потокобезопасными, этот вопрос не задает этого.Спрашивается, как реализация обеспечивает это, и использует ли она блокировки и проверки под капотом (эти блокировки не являются блокировками в строгом смысле слова, а скорее блокировками, используемыми реализацией JIT / CLR / other).

Ответы [ 2 ]

0 голосов
/ 28 октября 2018

Давайте сначала рассмотрим различные виды статических конструкторов и правила, которые указывают, когда каждый из них должен быть выполнен. Существует два вида статических конструкторов: Precise и BeforeFieldInit . Статические конструкторы, которые явно определены, являются точными. Если класс имеет инициализированные статические поля без явно определенного статического конструктора, то компилятор управляемого языка определяет тот, который выполняет инициализацию этих статических полей. Точные конструкторы должны выполняться непосредственно перед доступом к любому полю или вызову любого метода типа. Конструкторы BeforeFieldInit должны выполняться до доступа к первому статическому полю. Теперь я расскажу, когда и как статические конструкторы вызываются в CoreCLR и CLR.

Когда метод вызывается в первый раз, вызывается временная точка входа для этого метода, которая в основном отвечает за JIT-код IL-метода. Временная точка входа (в частности, prestub) проверяет тип статического конструктора типа вызываемого метода (независимо от того, является ли этот метод экземпляром static). Если он точный, то временная точка входа гарантирует, что статический конструктор этого типа был выполнен.

Затем временная точка входа вызывает JIT-компилятор для генерации собственного кода метода (поскольку он вызывается впервые). Компилятор JIT проверяет, включает ли IL метода доступ к статическим полям. Для каждого доступного статического поля, если статическим конструктором типа, который определяет это статическое поле, является BeforeFieldInit, то компилятор гарантирует, что статический конструктор типа был выполнен. Следовательно, нативный код метода не включает никаких вызовов статического конструктора. В противном случае, если статический конструктор типа, который определяет это статическое поле, является Precise, JIT-компилятор внедряет вызовы статического конструктора перед каждым доступом к статическому полю в собственном коде метода.

Статические конструкторы выполняются путем вызова CheckRunClassInitThrowing . Эта функция в основном проверяет, был ли тип уже инициализирован, и если нет, она вызывает DoRunClassInitThrowing , который фактически вызывает статический конструктор. Перед вызовом статического конструктора необходимо получить блокировку, связанную с этим конструктором. Существует один такой замок для каждого типа. Однако эти замки создаются лениво. То есть, только когда вызывается статический конструктор типа, блокировка создается для этого типа. Следовательно, список блокировок должен динамически поддерживаться для каждого домена приложения, а сам этот список должен быть защищен блокировкой. Поэтому вызов статического конструктора включает в себя две блокировки: блокировку, специфичную для приложения, и блокировку, зависящую от типа. Следующий код показывает, как эти две блокировки получаются и снимаются (некоторые мои комментарии).

void MethodTable::DoRunClassInitThrowing()
{

    .
    .
    .

    ListLock *_pLock = pDomain->GetClassInitLock();

    // Acquire the appdomain lock.
    ListLockHolder pInitLock(_pLock);

    .
    .
    .

    // Take the lock
    {
        // Get the lock associated with the static constructor or create new a lock if one has not been created yet.
        ListLockEntryHolder pEntry(ListLockEntry::Find(pInitLock, this, description));

        ListLockEntryLockHolder pLock(pEntry, FALSE);

        // We have a list entry, we can release the global lock now
        pInitLock.Release();

        // Acquire the constructor lock.
        // Block if another thread has the lock.
        if (pLock.DeadlockAwareAcquire())
        {
            .
            .
            .
        }

        // The constructor lock gets released by calling the destructor of pEntry.
        // The compiler itself emits a call to the destructor at the end of the block
        // since pEntry is an automatic variable.
    }

    .
    .
    .

}

Статические конструкторы нейтральных для приложений типов и NGEN-типов обрабатываются по-разному. Кроме того, реализация CoreCLR не строго придерживается семантики конструкторов Precise по соображениям производительности. Для получения дополнительной информации см. Комментарий вверху corinfo.h .

0 голосов
/ 27 октября 2018

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

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

Разве это не будет очень медленно?

private static void Main(string[] args)
{
   var t1 = Task.Run(
      () =>
         {
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 1");
            var val = Test.Value;
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 1 complete");
            return val;
         });
   var t2 = Task.Run(
      () =>
         {
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 2");
            var val = Test.Value;
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 2 complete");
            return val;
         });
   Task.WaitAll(t2, t2);
}

public static class Test
{
   static Test()
   {
      Thread.Sleep(2000);
      Value = 1;
   }

   public static int Value { get; }
}

выход

09:24:24.3817636 here 2
09:24:24.3817636 here 1
09:24:26.3866223 here 2 complete
09:24:26.3866223 here 1 complete

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


Технические характеристики ECMA

15.12 Статические конструкторы

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

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

...

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

Нет упоминания о том, как он выполняет исключительность (как и следовало ожидать), поскольку это просто деталь реализации, однако мы знаем, что это делает .

И, наконец, потому что просмотр спецификаций - бочка веселья и веселья (отдельные результаты могут отличаться), вы можете столкнуться с более странными ситуациями, такими как создание круговых зависимостей

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

class A
{
   public static int X;
   static A()
   {
      X = B.Y + 1;
   }
}
class B
{
   public static int Y = A.X + 1;
   static B() { }
   static void Main()
   {
      Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);
   }
}

производит вывод

X = 1, Y = 2

Чтобы выполнить метод Main, система сначала запускает инициализатор для B.Y, до статического конструктора класса B. Инициализатор Y вызывает А статический конструктор, который будет запущен из-за ссылки на значение A.X.

Статический конструктор A, в свою очередь, продолжает вычислять значение X, и при этом извлекает значение по умолчанию Y, которое равно нулю. A.x таким образом инициализируется в 1. Процесс запуска статического поля А инициализаторы и статический конструктор затем завершается, возвращаясь к Расчет начального значения Y, результатом которого становится 2.

...