Почему нет взаимоблокировки в этом сценарии?

Итак, я с удовольствием читал this от Эрика Липперта, а затем, конечно, отличные комментарии и в них Джон Пейсон сказал:

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

и я подумал, да, это было бы легко, поэтому я сбил это:

public static class A
{     
    static A()
    {
        Console.WriteLine("A.ctor");
        B.Initialize();
        Console.WriteLine("A.ctor.end");
    }

    public static void Initialize()
    {
        Console.WriteLine("A.Initialize");
    }
}
public static class B
{
    static B()
    {
        Console.WriteLine("B.ctor");
        A.Initialize();
        Console.WriteLine("B.ctor.end");
    }

    public static void Initialize()
    {
        Console.WriteLine("B.Initialize");
    }

    public static void Go()
    {
        Console.WriteLine("Go");
    }
}

Выход которого (после вызова B.Go()):

B.ctor
A.ctor
B.Initialize
A.ctor.end
A.Initialize
B.ctor.end
Go

Нет тупика, и я, очевидно, проигравший, поэтому, чтобы увековечить смущение, вот мой вопрос: почему здесь нет тупика?

Кажется моему маленькому мозгу, что B.Initialize называется до, статический конструктор B завершен, и я думал, что это не разрешено.

Ответ 1

Важным моментом является то, что задействован только один поток. Цитата из сообщения в блоге:

Затем статический конструктор запускает новый поток. Когда этот поток начинается, CLR видит, что статический метод будет вызван на тип, статический конструктор которого находится "в полете" другой поток. Он немедленно блокирует новый поток, так что метод Initialize не запускается до тех пор, пока основной поток не завершит выполнение конструктора класса.

В примере Erics есть два потока, ожидающих друг друга. У вас есть только один поток, поэтому нет ожиданий и, как следствие: без блокировки и без тупика.

Ответ 2

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

Вы не используете какие-либо ресурсы из A внутри B и наоборот. В результате ваша циклическая зависимость "безопасна" в том смысле, что ничто не взорвется.

Если вы проследите путь, который показывают ваши распечатки, ничто не должно блокироваться:

  • Вызов Go (я подозреваю)
  • Введите B (static конструктор), поскольку он не инициализирован.
  • Распечатайте
  • Используйте A.Initialize() Конструктор
  • A static должен выполнить первый
  • Распечатайте
  • Используйте B.Initialize()
  • B не нужно инициализировать, но он не находится в завершенном состоянии (к счастью, никаких переменных не задано, поэтому ничего не сломается)
  • Распечатайте, затем верните
  • Распечатайте (из A static конструктор), затем верните
  • A.Initialize() может, наконец, вызываться, потому что A инициализируется
  • Распечатайте, затем верните
  • Распечатайте (из B static конструктор), затем верните
  • Go

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

Ответ 3

Почему вы думаете, что должен быть тупик. Статические конструкторы называются только один раз. Независимо от того, сколько раз вы выполняете оператор B.Initialize();, он будет вызывать статический конструктор класса B только в первый раз, когда указывается B. Подробнее о статических конструкторах здесь.

Ответ 4

Статические методы вызывается только один раз и один раз загружаются, они не вызываются снова. Это был бы тупик, если метод A.Initialize() называется методом B.Initialize() и наоборот.

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

Ответ 5

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