Можно ли читать общий логический флаг, не блокируя его, когда другой поток может установить его (не более одного раза)? - PullRequest
42 голосов
/ 09 февраля 2012

Я бы хотел, чтобы мой поток отключился более изящно, поэтому я пытаюсь реализовать простой механизм сигнализации.Я не думаю, что мне нужен поток, полностью управляемый событиями, поэтому у меня есть рабочий с методом, который изящно останавливает его, используя критическую секцию Monitor (эквивалент C # lock, я полагаю):

DrawingThread.h

class DrawingThread {
    bool stopRequested;
    Runtime::Monitor CSMonitor;
    CPInfo *pPInfo;
    //More..
}

DrawingThread.cpp

void DrawingThread::Run() {
    if (!stopRequested)
        //Time consuming call#1
    if (!stopRequested) {
        CSMonitor.Enter();
        pPInfo = new CPInfo(/**/);
        //Not time consuming but pPInfo must either be null or constructed. 
        CSMonitor.Exit();
    }
    if (!stopRequested) {
        pPInfo->foobar(/**/);//Time consuming and can be signalled
    }
    if (!stopRequested) {
        //One more optional but time consuming call.
    }
}


void DrawingThread::RequestStop() {
    CSMonitor.Enter();
    stopRequested = true;
    if (pPInfo) pPInfo->RequestStop();
    CSMonitor.Exit();
}

Я понимаю (по крайней мере в Windows) Monitor / lock s - наименее дорогой примитив синхронизации потоков, но ястремится избежать чрезмерного использования.Должен ли я обернуть каждое чтение этого логического флага?Инициализируется как false и устанавливается только один раз, когда истина запрашивается остановка (если она запрашивается до завершения задачи).

Моим преподавателям рекомендовано защищать даже bool, потому что чтение / запись может быть невозможныматомное.Я думаю, что этот флаг с одним выстрелом является исключением, которое подтверждает правило?

Ответы [ 4 ]

45 голосов
/ 09 февраля 2012

Никогда не нормально читать что-то, возможно, измененное в другом потоке без синхронизации.Какой уровень синхронизации необходим, зависит от того, что вы на самом деле читаете.Для примитивных типов вы должны взглянуть на атомарные операции чтения, например, в виде std::atomic<bool>.

. Причина, по которой синхронизация всегда необходима, заключается в том, что процессоры будут иметь данные, которые могут совместно использоваться в строке кэша.У него нет причины обновлять это значение до значения, возможно, измененного в другом потоке, если нет синхронизации.Хуже того, если синхронизация отсутствует, она может записать неправильное значение, если что-то, хранящееся рядом со значением, будет изменено и синхронизировано.

11 голосов
/ 09 февраля 2012

Булево присваивание является атомарным. Это не проблема.

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

Решением является ограничение памяти, которое действительно неявно добавляется операторами блокировки, но для одной переменной это избыточно. Просто объявите это как std::atomic<bool>.

6 голосов
/ 09 февраля 2012

Ответ, я полагаю, "это зависит". Если вы используете C ++ 03, потоки не определены в Стандарте, и вам придется читать то, что говорит ваш компилятор и ваша библиотека потоков, хотя такого рода вещи обычно называют «доброкачественной гонкой» и обычно ОК .

Если вы используете C ++ 11, доброкачественные гонки - неопределенное поведение. Даже когда неопределенное поведение не имеет смысла для базового типа данных. Проблема заключается в том, что компиляторы могут предполагать, что программы не имеют неопределенного поведения, , и выполнять оптимизацию на основе этого (см. Также часть 1 и часть 2, связанные оттуда). Например, ваш компилятор может решить прочитать флаг один раз и кэшировать значение, потому что запись в переменную в другом потоке неопределенного поведения без какого-либо мьютекса или барьера памяти.

Конечно, вполне возможно, что ваш компилятор обещает не проводить эту оптимизацию. Вам нужно будет посмотреть.

Самое простое решение - использовать std::atomic<bool> в C ++ 11 или что-то вроде atomic_ops Ханса Бома в других местах.

1 голос
/ 09 февраля 2012

Нет, вы должны защищать каждый доступ, поскольку современные компиляторы и процессоры переупорядочивают код без учета ваших многопоточных задач. Доступ на чтение из разных потоков может работать, но не обязательно.

...