Нужно ли блокировать или маркировать как изменчивые при доступе к простому булевому флагом в С#?
Давайте просто скажем, что у вас простая операция, выполняемая в фоновом потоке. Вы хотите предоставить способ отменить эту операцию, чтобы создать флагов boolean, который вы установили в true из обработчика события клика кнопки отмены.
private bool _cancelled;
private void CancelButton_Click(Object sender ClickEventArgs e)
{
_cancelled = true;
}
Теперь вы устанавливаете флаг отмены из потока графического интерфейса, но вы читаете его из фонового потока. Вам нужно заблокировать перед доступом к bool?
Вам нужно сделать это (и, очевидно, заблокировать обработчик события нажатия кнопки):
while(operationNotComplete)
{
// Do complex operation
lock(_lockObject)
{
if(_cancelled)
{
break;
}
}
}
Или это приемлемо для этого (без блокировки):
while(!_cancelled & operationNotComplete)
{
// Do complex operation
}
Или о том, чтобы пометить переменную _cancelled как изменчивую. Это необходимо?
[Я знаю, что есть класс BackgroundWorker с его встроенным методом CancelAsync(), но меня интересует семантика и использование доступа к блокировке и потоковой переменной здесь, а не конкретная реализация, код - всего лишь пример.]
Кажется, есть две теории.
1) Поскольку это простой встроенный тип (и доступ к встроенным типам является атомарным в .net), и поскольку мы пишем только в одном месте и только чтение в фоновом потоке, нет необходимости блокировать или отмечать как изменчивый.
2) Вы должны пометить его как volatile, потому что если вы не компилятор, он может оптимизировать чтение в цикле while, потому что он не считает, что он способен изменять значение.
Какая правильная техника? (И почему?)
[Редактировать: На этом, кажется, две четко определенные и противоположные школы мысли. Я ищу окончательный ответ на это, поэтому, пожалуйста, по возможности укажите свои причины и укажите источники вместе с вашим ответом.]
Ответы
Ответ 1
Во-первых, потоки сложны; -p
Да, несмотря на все слухи об обратном, требуется либо использовать lock
, либо volatile
(но не оба) при доступе к bool
из нескольких потоков.
Для простых типов и доступа, таких как флаг выхода (bool
), достаточно volatile
- это гарантирует, что потоки не кэшируют значение в своих регистрах (что означает: один из потоков никогда не видит обновлений).
Для больших значений (когда атомарность является проблемой) или где вы хотите синхронизировать последовательность операций (типичным примером является "если не существует и добавить" доступ к словарю "), lock
является более универсальным. Это действует как барьер памяти, поэтому дает вам безопасность потока, но предоставляет другие функции, такие как импульс/ожидание. Обратите внимание, что вы не должны использовать lock
для типа значения или string
; ни Type
или this
; лучший вариант - иметь собственный объект блокировки в качестве поля (readonly object syncLock = new object();
) и заблокировать его.
Пример того, как сильно он ломается (т.е. циклически навсегда), если вы не синхронизируете - см. здесь.
Чтобы охватить несколько программ, может быть полезен примитив ОС, такой как Mutex
или *ResetEvent
, но это избыток для одного exe.
Ответ 2
_cancelled
должен быть volatile
. (если вы не хотите блокировать)
Если один поток изменяет значение _cancelled
, другие потоки могут не увидеть обновленный результат.
Кроме того, я думаю, что операции чтения/записи _cancelled
являются атомарными:
В разделе 12.6.6 спецификации CLI указано: "Соответствующий CLI должен гарантировать, что доступ к чтению и записи выровненная ячейка памяти не больше чем размер родного слова является атомарным когда все обращения на запись к местоположение того же размера."
Ответ 3
Блокировка не требуется, потому что у вас сценарий с одним сценарием, а логическое поле - это простая структура без риска развращения состояния (пока это возможно чтобы получить логическое значение, которое не является ни ложным, ни истинным). Но вы должны пометить это поле как volatile
, чтобы компилятор не выполнял некоторые оптимизации. Без модификатора volatile
компилятор может кэшировать значение в регистре во время выполнения вашего цикла в рабочем потоке, и в свою очередь цикл никогда не узнает измененное значение. Эта статья MSDN (Как создавать и завершать потоки (руководство по программированию на С#)) решает эту проблему.
Несмотря на необходимость блокировки, блокировка будет иметь тот же эффект, что и поле volatile
.
Ответ 4
Для синхронизации потоков рекомендуется использовать один из классов EventWaitHandle
, например ManualResetEvent
. В то время как немного проще использовать простой логический флаг, как вы здесь (и да, вы хотели бы отметить его как volatile
), ИМО лучше освоиться с использованием инструментов потоковой передачи. Для ваших целей вы бы сделали что-то вроде этого...
private System.Threading.ManualResetEvent threadStop;
void StartThread()
{
// do your setup
// instantiate it unset
threadStop = new System.Threading.ManualResetEvent(false);
// start the thread
}
В вашей теме..
while(!threadStop.WaitOne(0) && !operationComplete)
{
// work
}
Затем в GUI для отмены...
threadStop.Set();
Ответ 5
Посмотрите Interlocked.Exchange(). Он делает очень быструю копию в локальной переменной, которую можно использовать для сравнения. Это быстрее, чем lock().