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

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

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

Скажем, у нас есть class вроде:

 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_; }; 

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

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

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

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

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

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

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

Microsoft

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Interesting Posts

request.getQueryString (), похоже, нуждается в некоторой кодировке

Опубликованный Android apk дает ошибку «Файл пакета не был подписан правильно»

Должны использоваться бесполезные classификаторы типов по типам возврата для ясности?

Как заблокировать файл с помощью java (если возможно)

Почему я не могу использовать управление заданиями в сценарии bash?

Загрузить 32-битную библиотеку DLL в 64-битном приложении

Как использовать JarOutputStream для создания JAR-файла?

Как представить номер FLOAT в памяти на C

Приоритет && над ||

Как сделать глубокую копию объекта в Java?

Как преобразовать stream Java 8 в массив?

Как изменить зарегистрированное имя в Windows 7 на мое собственное?

Что такое инструмент для получения полного каталога / списка файлов с подробной информацией, включая хеш (ы)?

Нежелательное (неохотное) соответствие регулярному выражению в sed?

Как я могу использовать SRT для кэширования не загружаемого жесткого диска?

Давайте будем гением компьютера.