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

Допустим, у вас есть простая операция, которая выполняется в фоновом потоке. Вы хотите предоставить способ отменить эту операцию, чтобы создать логический флаг, для которого вы установили значение 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 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

Для синхронизации потоков рекомендуется использовать один из 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
}

Тогда в GUI отменить...

threadStop.Set();

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

Другие вопросы по тегам