Ответ 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 делится памятью по потокам. Есть тысячи и еще одно, о чем нужно беспокоиться в первую очередь.