Нужно ли блокировать или помечать как энергозависимые при доступе к простому логическому флагу в C #? - PullRequest
42 голосов
/ 03 августа 2009

Допустим, у вас есть простая операция, которая выполняется в фоновом потоке. Вы хотите предоставить способ отменить эту операцию, чтобы создать логический флаг, для которого вы установили значение true из обработчика события click кнопки отмены.

private bool _cancelled;

private void CancelButton_Click(Object sender ClickEventArgs e)
{
    _cancelled = true;
}

Теперь вы устанавливаете флаг отмены из потока GUI, но читаете его из фонового потока. Вам нужно заблокировать доступ к bool?

Нужно ли вам это сделать (и, очевидно, также заблокировать обработчик события нажатия кнопки):

while(operationNotComplete)
{
    // Do complex operation

    lock(_lockObject)
    {
        if(_cancelled)
        {
            break;
        }
    }
}

Или это допустимо (без блокировки):

while(!_cancelled & operationNotComplete)
{
    // Do complex operation
}

Или как пометить переменную _cancelled как volatile. Это необходимо?

[Я знаю, что есть класс BackgroundWorker с его встроенным методом CancelAsync (), но меня интересует семантика и использование блокировки и доступа к многопоточным переменным, а не конкретная реализация, код является лишь примером.]

Кажется, есть две теории.

1) Поскольку это простой встроенный тип (а доступ к встроенным типам является атомарным в .net) и поскольку мы пишем в него только в одном месте и читаем только в фоновом потоке, нет необходимости блокировать или отмечать как летучие.
2) Вы должны пометить его как volatile, потому что если вы этого не сделаете, компилятор может оптимизировать чтение в цикле while, потому что он ничего не думает, что он способен изменить значение.

Какая техника правильная? (А почему?)

[Редактировать: Кажется, есть две четко определенные и противоположные точки зрения по этому вопросу. Я ищу точный ответ на этот вопрос, поэтому, если возможно, опубликуйте свои причины и приведите источники вместе с ответом.]

Ответы [ 5 ]

37 голосов
/ 03 августа 2009

Во-первых, многопоточность сложна; -p

Да, несмотря на все слухи об обратном, является обязательным для либо использования lock или volatile (но не обоих), когда доступ к bool из нескольких потоков.

Для простых типов и доступа, таких как флаг выхода (bool), тогда достаточно volatile - это гарантирует, что потоки не кэшируют значение в своих регистрах (то есть: один из потоков никогда не видит обновлений) .

Для больших значений (где атомарность является проблемой) или когда вы хотите синхронизировать последовательность операций (типичный пример - доступ к словарю «если не существует и добавить»), lock более универсален Это действует как барьер памяти, поэтому все еще обеспечивает вам безопасность потока, но предоставляет другие функции, такие как импульс / ожидание. Обратите внимание, что вы не должны использовать lock для типа значения или string; ни Type или this; лучший вариант - иметь свой собственный объект блокировки в виде поля (readonly object syncLock = new object();) и заблокировать его.

Например, как сильно он ломается (т. Е. Зацикливается навсегда), если вы не синхронизируете - см. Здесь .

Для охвата нескольких программ может быть полезен примитив ОС, такой как Mutex или *ResetEvent, но для одного exe-файла это излишне.

6 голосов
/ 03 августа 2009

_cancelled должно быть volatile. (если вы не хотите заблокировать)

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

Кроме того, я думаю, что операции чтения / записи _cancelled являются атомными :

Раздел 12.6.6 спецификации CLI гласит: «Соответствующий CLI должен гарантировать, что доступ для чтения и записи, чтобы правильно выровненные ячейки памяти не больше чем родной размер слова является атомным когда все записи обращаются к расположение одного размера. "

5 голосов
/ 03 августа 2009

Блокировка не требуется, поскольку у вас есть сценарий с одним писателем, а логическое поле - это простая структура без риска повреждения состояния (, хотя можно было получить логическое значение, которое не является ни ложным, ни истинным ). Но вы должны пометить поле как volatile, чтобы компилятор не выполнял некоторые оптимизации. Без модификатора volatile компилятор может кэшировать значение в регистре во время выполнения вашего цикла в вашем рабочем потоке, и, в свою очередь, цикл никогда не распознает измененное значение. Эта статья MSDN ( Как: создавать и завершать потоки (Руководство по программированию в C #) ) решает эту проблему. Несмотря на необходимость блокировки, блокировка будет иметь тот же эффект, что и маркировка поля volatile.

2 голосов
/ 03 августа 2009

Для синхронизации потоков рекомендуется использовать один из классов EventWaitHandle, например ManualResetEvent. Хотя использовать простой логический флаг, как вы здесь, немного проще (и да, вы хотите пометить его как volatile), IMO лучше освоить практику использования инструментов потоков. Для ваших целей вы бы сделали что-то вроде этого ...

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
}

Затем в графическом интерфейсе отменить ...

threadStop.Set();
1 голос
/ 03 августа 2009

Посмотрите вверх Interlocked.Exchange () . Это делает очень быстрое копирование в локальную переменную, которую можно использовать для сравнения. Это быстрее, чем блокировка ().

...