Противоречат ли взаимоблокировки статических конструкторов в C# стандарту ECMA CLI?

Вот раздел стандарта, который меня смущает: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf#page=178&zoom=auto,87,610%22

2.1. Если тип еще не инициализирован, попробуйте установить блокировку инициализации.

2.2.1. В случае неудачи проверьте, удерживает ли этот поток или любой другой поток, ожидающий завершения этого потока, блокировку.

2.2.2. Если это так, вернитесь, поскольку блокировка приведет к тупиковой ситуации. Этот поток теперь увидит состояние неполной инициализации для типа, но взаимоблокировка не возникнет.

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

public static class Foo {
    static Foo() {
        var otherThread = new Thread(() => { Thread.Sleep(1000); SomeFunction(); });
        otherThread.Start();
        otherThread.Join();
    }
    public static void SomeFunction() {
    }
}
class Program {
    static void Main() {
        Foo.SomeFunction();
    }
}

Согласно стандарту, я ожидаю, что произойдет следующее:

  1. Основной поток берет блокировку инициализации на Foo.
  2. Основной поток запускает статический конструктор Foo.
  3. Основной поток создает otherThread и запускает его.
  4. otherThread начинает ждать одну секунду, таким образом гарантируя, что точка 5 произойдет раньше, чем точка 6.
  5. Основной поток начинает ожидать завершения otherThread.
  6. otherThread пытается взять блокировку инициализации на Foo и терпит неудачу, потому что основной поток удерживает блокировку.
  7. otherThread отказывается от выполнения статического конструктора, потому что основной поток удерживает блокировку инициализации и ожидает otherThread.
  8. otherThread запускает SomeFunction и успешно завершает работу.
  9. Основная нить возвращается.

Что здесь не так?


person Winani    schedule 07.09.2018    source источник
comment
Обратите внимание, что этот раздел спецификации на самом деле является обоснованием, которое больше похоже на комментарий, чем на формальные гарантии или правила. В нем описывается гипотетический алгоритм для обеспечения фактической гарантии, но реализация не обязана его реализовывать. Фактическая гарантия заключается в том, что сама по себе инициализация типа не должна создавать взаимоблокировку, если некоторый код, вызываемый из инициализатора типа (прямо или косвенно), явно вызывает блокирующие операции. (выделено мной). Данный пример явно нарушает это условие, ожидая (Join) в инициализаторе.   -  person Mike Zboray    schedule 08.09.2018
comment
В комментариях к моей статье 2013 года на эту тему есть обсуждение этой проблемы: ericlippert.com/2013/01/31/the-no-lock-deadlock   -  person Eric Lippert    schedule 16.03.2019


Ответы (1)


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

Цитируемый раздел относится к тому факту, что приведенный ниже пример не будет заблокирован:

public class A
{
    static A()
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        B.DoNothing();
    }
    public static void DoNothing() { }
}
public class B
{
    static B()
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));
        A.DoNothing();
    }
    public static void DoNothing() { }
}
private static void Main()
{
    Task.Run(() => B.DoNothing());
    A.DoNothing();
}

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

person Servy    schedule 07.09.2018