Является ли volatile bool для управления потоками ошибочным? - PullRequest
26 голосов
/ 09 августа 2011

В результате моего ответа на на этот вопрос я начал читать о ключевом слове volatile и о его консенсусе.Я вижу, что есть много информации об этом, некоторая старая, которая кажется неправильной сейчас, и много новой, которая говорит, что ей почти нет места в многопоточном программировании.Следовательно, я хотел бы уточнить конкретное использование (не могу найти точный ответ здесь на SO).

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

Скажем, мыиметь класс вроде:

class SomeWorker
{
public:
    SomeWorker() : isRunning_(false) {}
    void start() { isRunning_ = true; /* spawns thread and calls run */ }
    void stop() { isRunning_ = false; }

private:
    void run()
    {
        while (isRunning_)
        {
            // do something
        }
    }
    volatile bool isRunning_;
};

Для простоты некоторые вещи опущены, но существенным является то, что создается объект, который делает что-то во вновь порожденном потоке, проверяя (volatile)логическое, чтобы знать, если это должно прекратиться.Это логическое значение устанавливается из другого потока всякий раз, когда он хочет, чтобы работник остановился.

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

Я хотел бы понять, считается ли это совершенно неправильным и правильный ли подход - использоватьсинхронизированная переменная? Есть ли разница между компилятором / архитектурой / ядрами?Может быть, это просто неаккуратный подход, которого стоит избегать?

Я был бы рад, если бы кто-то разъяснил это.Спасибо!

РЕДАКТИРОВАТЬ

Мне было бы интересно увидеть (в коде), как вы решите эту проблему.

Ответы [ 5 ]

9 голосов
/ 09 августа 2011

volatile может использоваться для таких целей. Однако это расширение стандарта C ++ от Microsoft :

Специфично для Microsoft

Объекты, объявленные как volatile, являются (...)

  • Запись в энергозависимый объект (энергозависимая запись) имеет семантику Release;(...)
  • Чтение летучего объекта (volatile read) имеет семантику Acquire;(...)

Это позволяет использовать энергозависимые объекты для блокировок и выпусков памяти в многопоточных приложениях. (добавлено emph.)

То есть, насколько я понимаю, когда вы используете компилятор Visual C ++, volatile bool для большинства практических целей является atomic<bool>.

ItСледует отметить, что в более новых версиях VS добавлен / volatile-переключатель , который управляет этим поведением, так что это верно только в том случае, если /volatile:ms активен.

8 голосов
/ 09 августа 2011

Вам не нужна синхронизированная переменная , а скорее атомарная переменная.К счастью, вы можете просто использовать std::atomic<bool>.

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

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

Редактировать: Вы не говорите в своем вопросе как bool обновляется, поэтому я предполагаю худшее.Если вы, например, заключите всю операцию обновления в глобальную блокировку, то, конечно, одновременного доступа к памяти не будет.

7 голосов
/ 09 августа 2011

Есть три основные проблемы, с которыми вы сталкиваетесь при многопоточности:

1) Синхронизация и безопасность потоков.Переменные, которые совместно используются несколькими потоками, должны быть защищены от записи в несколько потоков одновременно и защищены от чтения во время неатомарных записей.Синхронизация объектов может быть выполнена только через специальный объект семафора / мьютекса, который гарантированно сам по себе является атомарным.Ключевое слово volatile не помогает.

2) Трубопроводы с инструкциями.Процессор может изменить порядок выполнения некоторых инструкций, чтобы ускорить выполнение кода.В многопроцессорной среде, где один поток выполняется для каждого процессора, процессоры передают инструкции, не зная, что другой процессор в системе делает то же самое.Защита от передачи инструкций называется барьерами памяти.Все это хорошо объяснено в Википедии .Барьеры памяти могут быть реализованы либо через выделенные объекты барьера памяти, либо через объект семафора / мьютекса в системе.Компилятор, возможно, мог бы выбрать вызов барьера памяти в коде, когда используется ключевое слово volatile, но это было бы скорее специальным исключением, а не нормой.Я бы никогда не предположил, что ключевое слово volatile сделало это, не проверив его в руководстве по компилятору.

3) Незнание компилятором функций обратного вызова.Как и в случае аппаратных прерываний, некоторые компиляторы могут не знать, что была выполнена функция обратного вызова, и обновили значение в середине выполнения кода.Вы можете иметь такой код:

// main
x=true;
while(something) 
{   
  if(x==true)   
  {
    do_something();
  }
  else
  {
    do_seomthing_else();
    /* The code may never go here: the compiler doesn't realize that x 
       was changed by the callback. Or worse, the compiler's optimizer 
       could decide to entirely remove this section from the program, as
       it thinks that x could never be false when the program comes here. */
  } 
}

// thread callback function:
void thread (void)
{
  x=false;
}

Обратите внимание, что эта проблема появляется только на некоторых компиляторах, в зависимости от настроек оптимизатора.Эта конкретная проблема решается с помощью ключевого слова volatile.


Таким образом, ответ на вопрос таков: в многопоточной программе ключевое слово volatile не помогает с синхронизацией / безопасностью потоков, скорее всего, нетдействует как барьер памяти, но может предотвратить опасные предположения оптимизатора компилятора.

6 голосов
/ 09 августа 2011

Использование volatile достаточно только для одного ядра, где все потоки используют один и тот же кеш. В многоядерных системах, если stop() вызывается на одном ядре, а run() выполняется на другом, для синхронизации кэшей ЦП может потребоваться некоторое время, что означает, что два ядра могут видеть два разных представления isRunning_. Это означает, что run() будет работать некоторое время после его остановки.

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

0 голосов
/ 07 февраля 2013

Это будет работать для вашего случая, но для защиты критической секции этот подход неправильный. Если бы это было правильно, то можно использовать изменчивый bool почти во всех случаях, когда используется мьютекс. Причина этого в том, что переменная переменная не гарантирует применения каких-либо барьеров памяти или какого-либо механизма согласованности кэша. Наоборот, мьютекс делает. Другими словами, как только мьютекс заблокирован, аннулирование кэша передается всем ядрам, чтобы поддерживать согласованность между всеми ядрами. С изменчивым это не тот случай. Тем не менее, Андрей Александреску предложил очень интересный подход для использования volatile для обеспечения синхронизации на общем объекте. И, как вы увидите, он делает это с мьютексом; volatile используется только для предотвращения доступа к интерфейсу объекта без синхронизации.

...