2

В классе со статическими полями во время работы приложения инициализация этих статических полей происходит только один раз, что логично. Но отсюда вопрос: получается JIT-компилятор создает что-то типа булевой переменной, которая показывает: был ли уже вызван статический конструктор или нет, и при каждом обращении к статическим переменным проверяет эту переменную? Так же примерно работает и NGen?

Qutrix
  • 1,214
  • 1
    а зачем ему это где-то хранить? – Grundy Jan 27 '17 at 07:55
  • 2
    Там не просто булева переменная. Там еще и критическая секция для синхронизации :) – Pavel Mayorov Jan 27 '17 at 07:56
  • @Grundy, чтоб потом проверять при обращении к статическим переменным, наверное. Я не знаю, поэтому и задал этот вопрос) – Qutrix Jan 27 '17 at 08:02
  • @PavelMayorov, а про это можно где-нибудь подробнее прочитать на русском? И получается, что лучше со статическими полями не заигрываться, так как они несут какие-то накладные расходы? И вообще, есть ли возможность это отключить, чтоб я сам мог контролировать когда и чем инициализировать? – Qutrix Jan 27 '17 at 08:09
  • 3
    @Qutrix не додумывайте. Любой класс несет "какие-то накладные расходы" независимо от количества статических полей в нем. – Pavel Mayorov Jan 27 '17 at 08:11
  • Почитать у Рихтера, конечно. Загляните в список литературы. – VladD Jan 27 '17 at 08:37
  • @VladD, Вы про clr via c#? Хочу прочитать, но пока что интересует этот вопрос, не подскажите в какой главе прочитать? А то я искал уже и не находил, к сожалению – Qutrix Jan 27 '17 at 08:44
  • @Qutrix: Ага, в ней. У меня нет сейчас книги под рукой, но насколько мне помнится там должно быть всё, что надо. И даже больше. – VladD Jan 27 '17 at 08:48

1 Answers1

3

Из Рихтера, CLR via C#/4.5, глава 8, раздел «Конструкторы типов»:

У вызова конструктора типа есть некоторые особенности. При компиляции метода [любого — VladD] JIT-компилятор обнаруживает типы, на которые есть ссылки из кода. Если в каком-либо из типов определён [статический — VladD] конструктор, JIT-компилятор проверяет, был ли исполнен конструктор типа в данном домене приложений. Если нет, JIT-компилятор создаёт в IL-коде вызов конструктора типа. Если же код уже исполнялся, JIT-компилятор вызова конструктора типа не создаёт, так как «знает», что тип уже инициализирован.

Затем, после JIT-компиляции метода, начинается [дальнейшее — VladD] выполнение потока, и в конечном итоге очередь доходит до кода вызова конструктора типа. В реальности может оказаться, что несколько потоков одновременно начнут выполнять метод. CLR старается гарантировать, чтобы конструктор типа выполнялся только раз в каждом домене приложений. Для этого при вызове конструктора типа вызывающий поток получает исключающую блокировку. Это означает, что если несколько потоков одновременно попытаются вызвать конструктор типа, только один получит такую возможность, а остальные блокируются. Первый поток исполнит код статического конструктора. После выхода первого потока из конструктора «проснутся» простаивающие потоки и проверят, был ли выполнен конструктор. Они не станут снова выполнять код, а просто вернут управление из метода конструктора.. Кроме того, при последующем вызове какого-либо из этих методов CLR будет «в курсе», что конструктор типа уже выполнялся, и не будет вызывать его снова.

Таким образом:

  • вызов статического конструктора типа C неявно включается в нативный код метода, впервые по времени выполнения упоминающего тип C в приложении, при JIT-компиляции этого метода
  • на время выполнения статического конструктора потоку достаётся эксклюзивная блокировка, чтобы другие потоки не смогли зайти в тот же код
  • другие методы (или этот же), пытающиеся выполнить статический конструктор снова, на деле пропустят его, это забота CLR (например, JIT-тер может затереть код статического конструктора после его отработки).

Интересный пример от Эрика Липперта, показывающий, что игнорировать глобальную блокировку не всегда получится:

class C
{
    static C() 
    {
        // Let's run the initialization on another thread!
        var thread = new System.Threading.Thread(Initialize);
        thread.Start();
        thread.Join();
    }
    static void Initialize() { }
    static void Main() { }
}

В этом примере происходит deadlock.

Что происходит? При выполнении Main сначала запускается статический конструктор. В нём запускается фоновый поток, который выполняет метод Initialize. При JIT-компиляции метода Initialize JIT-тер видит, что метод принадлежит классу C, что статический конструктор ещё не окончен, и добавляет его вызов внутрь метода Initialize. Метод Initialize ждёт глобальной блокировки, чтобы выполнить статический конструктор (точнее, дождаться его окончания, чтобы получить полностью проинициализированный тип). Но глобальная блокировка не будет отпущена, т. к. статический конструктор ждёт завершения Initialize() в thread.Join().

VladD
  • 206,799