Подтвердить что ты не робот

Чтение Введение в С# - как защитить от него?

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

public class ReadIntro {
  private Object _obj = new Object();
  void PrintObj() {
    Object obj = _obj;
    if (obj != null) {
      Console.WriteLine(obj.ToString()); // May throw a NullReferenceException
    }
  }
  void Uninitialize() {
    _obj = null;
  }
}

Обратите внимание на комментарий "Может выдавать исключение NullReferenceException" - я никогда не знал, что это возможно.

Итак, мой вопрос: как я могу защитить от ознакомления?

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

4b9b3361

Ответ 1

Позвольте мне попытаться прояснить этот сложный вопрос, разбив его.

Что такое "прочитанное введение"?

"Читать введение" - это оптимизация, при которой код:

public static Foo foo; // I can be changed on another thread!
void DoBar() {
  Foo fooLocal = foo;
  if (fooLocal != null) fooLocal.Bar();
}

оптимизируется путем исключения локальной переменной. Компилятор может рассуждать, что если есть только один поток, то foo и fooLocal - одно и то же. Компилятору явно разрешено делать любую оптимизацию, которая была бы невидимой в одном потоке, даже если она становится видимой в многопоточном сценарии. Поэтому компилятору разрешено переписать это как:

void DoBar() {
  if (foo != null) foo.Bar();
}

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

Может ли это произойти?

Как связанная с вами статья вызывается:

Обратите внимание, что вы не сможете воспроизвести исключение NullReferenceException с использованием этого примера кода в .NET Framework 4.5 на x86-x64. Прочитать введение очень сложно воспроизвести в .NET Framework 4.5, но оно тем не менее происходит в определенных особых обстоятельствах.

Чипы

x86/x64 имеют "сильную" модель памяти, а jit-компиляторы не агрессивны в этой области; они не будут делать эту оптимизацию.

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

Когда вы говорите "компилятор", какой компилятор вы имеете в виду?

Я имею в виду jit-компилятор. Компилятор С# никогда не вводит чтение таким образом. (Это разрешено, но на практике это никогда не происходит.)

Разве это не плохая практика обмена памятью между потоками без барьеров памяти?

Да. Здесь нужно что-то сделать, чтобы ввести барьер памяти, поскольку значение foo уже может быть устаревшим кешированным значением в кеше процессора. Мое предпочтение введению барьера памяти заключается в использовании блокировки. Вы также можете создать поле volatile или использовать VolatileRead или использовать один из методов Interlocked. Все они вводят барьер памяти. (volatile вводит только FYI с половинной заборкой.)

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

Существуют ли другие опасности для этого шаблона?

Конечно! Предположим, что нет читаемых интродукций. У вас все еще есть состояние гонки. Что, если другой поток устанавливает foo в null после проверки, , а также изменяет глобальное состояние, которое Bar будет потреблять? Теперь у вас есть два потока, один из которых считает, что foo не является нулевым, а глобальное состояние подходит для вызова Bar, а другой поток, который считает наоборот, и вы используете Bar. Это рецепт катастрофы.

Итак, что лучше всего здесь?

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

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

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

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

Ответ 2

Вы не можете на самом деле "защищать" от чтения, поскольку это оптимизация компилятора (за исключением использования отладочных сборников без оптимизации курса). Это довольно хорошо документировано, что оптимизатор будет поддерживать однопоточную семантику функции, которая, как отмечает статья, может вызвать проблемы в многопоточных ситуациях.

Тем не менее, я смущен его примером. В Джеффри Рихтере книга CLR через С# (v3 в этом случае), в разделе "События" он охватывает этот шаблон и отмечает, что в приведенном выше фрагменте примера в ТЕОРИИ это не сработает. Но это была рекомендуемая модель Microsoft в начале существования .Net, поэтому люди-компиляторы JIT, с которыми он говорил, говорили, что им нужно будет убедиться, что такой фрагмент никогда не сломается. (Всегда возможно, что они могут решить, что это по какой-то причине ломается - я думаю, Эрик Липперт мог пролить свет на это).

Наконец, в отличие от статьи, Джеффри предлагает "правильный" способ справиться с этим в многопоточных ситуациях (я изменил его пример с помощью кода примера):

Object temp = Interlocked.CompareExchange(ref _obj, null, null);
if(temp != null)
{
    Console.WriteLine(temp.ToString());
}

Ответ 3

Я только просмотрел статью, но кажется, что автор ищет, что вам нужно объявить член _obj как volatile.