Является ли volatile bool для управления потоками неправильным?

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

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

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

ИЗМЕНИТЬ

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

Ответ 1

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

Спецификация Microsoft

Объекты, объявленные как изменчивые, являются (...)

  • Запись во volatile object (volatile write) имеет семантику Release; (...)
  • Чтение изменчивого объекта (volatile read) имеет семантику Acquire; (...)

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

Ответ 4

Перед многопоточным процессом возникают три основные проблемы:

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

2) Трубопроводы. ЦП может изменить порядок выполнения некоторых инструкций, чтобы сделать код быстрее. В многопроцессорной среде, где один поток выполняется для каждого процессора, инструкции по процессорам CPU, не зная, что другой процессор в системе делает то же самое. Защита от проводов команд называется барьерами памяти. Все это хорошо объясняется в Wikipedia. Пункты памяти могут быть реализованы либо через выделенные объекты барьера памяти, либо через объект семафора/мьютекса в системе. Компилятор мог бы выбрать вызов барьера памяти в коде при использовании ключевого слова 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 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 не помогает в синхронизации потоков/безопасности, оно, скорее всего, не действует как барьер памяти, но может помешать компромиссу с опасными предположениями оптимизатор.

Ответ 5

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