Предположим, что я хочу использовать флаг состояния boolean для совместной отмены между потоками. (Я понимаю, что лучше использовать CancellationTokenSource
вместо этого, это не вопрос этого вопроса.)
private volatile bool _stopping;
public void Start()
{
var thread = new Thread(() =>
{
while (!_stopping)
{
// Do computation lasting around 10 seconds.
}
});
thread.Start();
}
public void Stop()
{
_stopping = true;
}
Вопрос. Если я вызываю Start()
в 0s и Stop()
в 3s в другом потоке, будет ли цикл завершен в конце текущей итерации со скоростью около 10 секунд?
Подавляющее большинство источников, которые я видел, показывают, что вышеуказанное должно работать как ожидалось; видеть: MSDN; Jon Skeet; Брайан Гидеон; Марк Гравелл; Ремус Русану.
Тем не менее, volatile
генерирует только захват при чтении и освобождение при записи:
Волатильное чтение имеет "приобретать семантику"; то есть он гарантированно будет происходить до любых ссылок на память, которые происходят после него в последовательности команд. У изменчивой записи есть "семантика выпуска"; то есть, это гарантировано произойдет после любых ссылок на память до команды записи в последовательности команд. (Спецификация С#)
Таким образом, нет гарантии, что волатильная запись и изменчивое чтение не будут (по-видимому) заменены, как отмечено Joseph Albahari. Следовательно, возможно, что фоновый поток будет продолжать читать устаревшее значение _stopping
(а именно, false
) после окончания текущей итерации. Конкретно, если я назову Start()
в 0s и Stop()
на 3s, возможно, фоновая задача не будет завершена на 10 секунд, как ожидалось, но на 20 или 30 секунд или никогда вообще не будет.
Основываясь на приобретать и выпускать семантику, здесь есть две проблемы. Во-первых, волатильное чтение было бы ограничено обновлением поля из памяти (абстрактно говоря) не в конце текущей итерации, а в конце последующего, поскольку забор происходит после самого чтения. Во-вторых, более критически, нет ничего, чтобы заставить volatile write когда-либо передавать значение в память, поэтому нет никакой гарантии, что цикл вообще закончится.
Рассмотрим следующий поток последовательности:
Time | Thread 1 | Thread 2
| |
0 | Start() called: | read value of _stopping
| | <----- acquire-fence ------------
1 | |
2 | |
3 | Stop() called: | ↑
| ------ release-fence ----------> | ↑
| set _stopping to true | ↑
4 | ↓ | ↑
5 | ↓ | ↑
6 | ↓ | ↑
7 | ↓ | ↑
8 | ↓ | ↑
9 | ↓ | ↑
10 | ↓ | read value of _stopping
| ↓ | <----- acquire-fence ------------
11 | ↓ |
12 | ↓ |
13 | ↓ | ↑
14 | ↓ | ↑
15 | ↓ | ↑
16 | ↓ | ↑
17 | ↓ | ↑
18 | ↓ | ↑
19 | ↓ | ↑
20 | | read value of _stopping
| | <----- acquire-fence ------------
Наиболее важными частями являются заграждения памяти, отмеченные -->
и <--
, которые представляют точки синхронизации потока. Нестабильное считывание _stopping
может (только отображаться) перемещаться до самой нити за предыдущий забор. Тем не менее, волатильная запись может (как представляется,) перемещаться вниз на неопределенный срок, так как в ее потоке нет другого забора. Другими словами, нет " synchronizes-with" (отношение "бывает раньше", "есть-видимо-к" ) между напишите на _stopping
и любые его чтения.
P.S. Я знаю, что MSDN дает очень сильные гарантии для ключевого слова volatile
. Однако экспертный консенсус заключается в том, что MSDN неверна (и не подкреплена спецификацией ECMA):
В документации MSDN указано, что использование ключевого слова volatile "гарантирует, что самое актуальное значение всегда присутствует в поле". Это неверно, поскольку, как мы видели [в предыдущем примере], запись с последующим чтением может быть переупорядочена. (Джозеф Альбахари)