Глава.29 .Примитивные.конструкции.синхронизации.потоков
Метод
Main
в этом фрагменте кода создает новый поток, исполняющий метод
Worker
, который считает по возрастающей, пока не получит команду остановиться.
Метод
Main
позволяет потоку метода
Worker
работать 5 секунд, а затем останав-
ливает его, присваивая статическому полю
Boolean
значение
true
. В этот момент
поток метода
Worker
должен вывести результат счета, после чего он завершится.
Метод
Main
ждет завершения метода
Worker
, вызывает метод
Join
, после чего по-
ток метода
Main
возвращает управление, заставляя весь процесс прекратить работу.
Выглядит просто, не так ли? Но программа скрывает потенциальные проблемы
из-за возможных оптимизаций. При компиляции метода
Worker
компилятор обна-
руживает, что переменная
s_stopWorker
может принимать значение
true
или
false
,
но внутри метода это значение никогда не меняется. Поэтому компилятор может
создать код, заранее проверяющий состояние переменной
s_stopWorker
. Если она
имеет значение
true
, выводится результат
"Worker: stopped when x=0"
. В про-
тивном случае компилятор создает код, входящий в бесконечный цикл и бесконечно
увеличивающий значение переменной
x
. При этом оптимизация заставляет цикл
работать крайне быстро, так как проверка переменной
s_stopWorker
осуществляется
перед циклом, а проверки переменной на каждой итерации цикла не происходит.
Если вы хотите посмотреть, как это работает, поместите код в файл с расширени-
ем
cs
и скомпилируйте его с ключами
platform:x86
и
/optimize+
компилятора C#.
Запустите полученный исполняемый файл и вы убедитесь, что программа работает
бесконечно. Обратите внимание, что вам нужен JIT-компилятор для платформы
x86, который совершеннее компиляторов x64 и IA64, а значит, обеспечивает более
полную оптимизацию. Остальные JIT-компиляторы не выполняют оптимизацию
столь тщательно, поэтому после их работы программа успешно завершится. Это
подчеркивает еще один интересный аспект: итоговое поведение вашей программы
зависит от множества факторов, в частности от выбранной версии компилятора и ис-
пользуемых ключей, от выбранного JIT-компилятора, от процессора, который будет
выполнять код. Кроме того, программа не станет работать бесконечно, если запустить
ее в отладчике, так как отладчик заставляет JIT-компилятор ограничиться неопти-
мизированным кодом, который проще поддается выполнению в пошаговом режиме.
Рассмотрим другой пример, в котором пара потоков осуществляет доступ к двум
полям:
internal sealed class ThreadsSharingData {
private Int32 m_flag = 0;
private Int32 m_value = 0;
// Этот метод исполняется одним потоком
public void Thread1() {
// ПРИМЕЧАНИЕ. Они могут выполняться в обратном порядке
m_value = 5;
m_flag = 1;
}
// Этот метод исполняется другим потоком
829
Конструкции.пользовательского.режима
public void Thread2() {
// ПРИМЕЧАНИЕ. Поле m_value может быть прочитано раньше, чем m_flag
if (m_flag == 1)
Console.WriteLine(m_value);
}
}
В данном случае проблема в том, что компиляторы и процессор могут оттрансли-
ровать код таким образом, что две строки в методе
Thread1
поменяются местами.
Разумеется, это не изменит предназначения метода. Метод должен получить зна-
чение 5 в переменной
m_value
и значение 1 в переменной
m_flag
. С точки зрения
однопоточного приложения порядок выполнения строк кода не имеет значения.
Если же поменять указанные строки местами, другой поток, выполняющий метод
Thread2
,
может
обнаружить, что переменная
m_flag
имеет значение 1, и выведет
значение 0.
Рассмотрим этот код с другой точки зрения. Предположим, что код метода
Thread1
выполняется так, как
предусмотрено программой
(то есть так, как он на-
писан). Обрабатывая код метода
Thread2
, компилятор должен сгенерировать код,
читающий значения переменных
m_flag
и
m_value
из оперативной памяти в ре-
гистры процессора. И возможно, что память первой выдаст значение переменной
m_value
, равное 0. Затем может выполниться метод
Thread1
, меняющий значение
переменной
m_value
на 5, а переменной
m_flag
— на 1. Но регистр процессора ме-
тода
Thread2
не видит, что значение переменной
m_value
было изменено другим
потоком на 5. После этого из оперативной памяти в регистре процессора может
быть считано значение переменной
m_flag
, ставшее равным 1. В результате метод
Thread2
снова выведет значение 0.
Все эти крайне неприятные события, скорее всего, приведут к проблемам в окон-
чательной, а не в отладочной версии программы. В результате задача выявления
проблемы и исправления кода становится нетривиальной. Поэтому сейчас давайте
поговорим о том, как исправить код.
Класс
System.Threading.Volatile
содержит два статических метода, которые
выглядят следующим образом
1
:
public sealed class Volatile {
public static void Write(ref Int32 location, Int32 value);
public static Int32 Read(ref Int32 location);
}
Это специальные методы, отключающие оптимизации, обычно выполняемые
компилятором C#, JIT-компилятором и собственно процессором. Вот как они
работают:
1
Существуют также перегруженные версии методов VolatileRead и VolatileWrite, рабо-
тающие с типами: Boolean, (S)Byte, (U)Int16, UInt32, (U)Int64, (U)IntPtr, Single, Double
и T, где T — обобщенный тип с ограничением 'class' (ссылочные типы).
830
Do'stlaringiz bilan baham: |