Как безопасно обходить циклы BeforeFieldInit и статического конструктора? - PullRequest
3 голосов
/ 15 марта 2019

Я обеспокоен взаимодействием между следующими двумя поведениями:

http://www.ecma -international.org / публикации / файлы / ECMA-ST / ECMA-335.pdf # страница = 179

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

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

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

http://www.ecma -international.org / публикации / файлы / ECMA-ST / ECMA-335.pdf # страница = 69

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

Этот пример кода демонстрирует возможное взаимодействие:

static class Foo {
  public static int A = 1;
}
static class Bar {
  public static int B = Foo.A;
}
static class Program {
  static void Main() {
    Console.WriteLine(Bar.B);
  }
}

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

  1. Main начинает выполнение.
  2. Инициализатор типа для Foo начинает выполняться (это может произойти в настоящее время из-за правила BeforeFieldInit).
  3. Инициализатор типа для Bar начинает выполняться (это может произойти в настоящее время из-за правила BeforeFieldInit).
  4. Инициализатор для Bar.B начинает выполняться.
  5. Foo.A запрашивается.
  6. Инициализатор типа для Foo уже запущен, и его ожидание может привести к тупику. Правило взаимоблокировки позволяет нам смотреть на Foo в не полностью инициализированном состоянии, где A еще не установлено в 1 и все еще имеет значение по умолчанию 0.
  7. Bar.B установлен на 0.
  8. Инициализация типа для Bar завершена.
  9. Foo.A установлено на 1.
  10. Инициализация типа для Foo завершена.
  11. Основные выходы Bar.B, что равно 0.

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

1 Ответ

3 голосов
/ 16 марта 2019

Это действительно разрешено?

Похоже, что это разрешено спецификацией.

При тестировании в любой нормальной среде выводится 1.

Это верно. Полезно понять причины оптимизации. Цель "расслабленной семантики" состоит в том, чтобы перенести проверку на "запущен ли статический конструктор?" от время выполнения доступа к типу до время выполнения метода, который обращается к типу . То есть, если мы имеем:

void M()
{
    blah
    if blah blah
    ... Foo.A ...
    if blah blah blah
    ... Foo.A ...
    blah blah blah
}

Предположим, что это время для M, а cctor Foo еще не выполнен. Для строгого соответствия джиттер должен генерировать код на при каждом доступе к Foo.A , чтобы проверить, выполнил ли уже Foo cctor, и выполнить его, если он этого не сделал.

Но если мы сделаем вызов cctor во время jit , jitter знает, что к Foo обращаются внутри M, и поэтому может вызвать cctor, когда M jipped, а затем пропустить генерацию каждой проверки внутри M

Джиттер достаточно умен, чтобы делать правильные вещи при выполнении cctor во время jit; он не выполняет cctor в «неправильном» порядке, как вы описываете, потому что люди, которые написали джиттер, были здравомыслящими людьми, которые просто пытались сделать ваш код быстрее.

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

Вы должны предположить, что авторы соответствующей реализации вменяемы.

Если по какой-либо причине вы не можете предположить, что: вы можете поместить все инициализаторы статических полей, которые вам нужны, в статический конструктор. Компилятор C # не допускает семантику BeforeFieldInit для типов со статическими конструкторами.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...