Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 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().