Почему volatile не считается полезным в многопоточном программировании на C или C ++?

165

Как показано в этом ответе, который я недавно опубликовал, мне кажется, что я не совсем понимаю полезность (или ее отсутствие) volatileмногопоточного программирования.

Насколько я понимаю, в любой момент, когда переменная может быть изменена вне потока контроля фрагмента кода, обращающегося к ней, эта переменная должна быть объявлена volatile. Обработчики сигналов, регистры ввода / вывода и переменные, измененные другим потоком, представляют собой такие ситуации.

Итак, если у вас есть глобальный int foo, и fooон читается одним потоком и атомарно устанавливается другим потоком (возможно, с использованием соответствующей машинной инструкции), поток чтения видит эту ситуацию так же, как он видит переменную, настроенную обработчиком сигнала или изменено внешним аппаратным условием и, таким образом, fooдолжно быть объявлено volatile(или, для многопоточных ситуаций, доступ к которому осуществляется с нагрузкой из памяти, что, вероятно, является лучшим решением)

Как и где я не прав?

Майкл Экстранд
источник
7
Все, что делает volatile, это говорит о том, что компилятор не должен кэшировать доступ к переменной volatile. Это ничего не говорит о сериализации такого доступа. Это обсуждалось здесь, я не знаю, сколько раз, и я не думаю, что этот вопрос что-то добавит к этим обсуждениям.
4
И опять же, вопрос, который не заслуживает этого, и который задавался здесь много раз, прежде чем получить голосование. Пожалуйста, прекратите это делать.
14
@neil Я искал другие вопросы и нашел один, но любое существующее объяснение, которое я видел, каким-то образом не вызывало того, что мне нужно, чтобы действительно понять, почему я был неправ. Этот вопрос вызвал такой ответ.
Майкл Экстранд
1
Для подробного изучения того, что процессоры делают с данными (через их кеши), посмотрите: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf
Sassafras_wot
1
@curiousguy Вот что я имел в виду под «не так в C», где он может использоваться для записи в аппаратные регистры и т. д. и не используется для многопоточности, как это обычно используется в Java.
Монстер

Ответы:

213

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

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

Для поточно-ориентированного доступа к общим данным нам нужна гарантия того, что:

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

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

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

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

Начиная с C ++ 11, атомарные переменные ( std::atomic<T>) дают нам все соответствующие гарантии.

jalf
источник
5
@jbcreix: О каком «этом» ты спрашиваешь? Изменчивые или барьеры памяти? В любом случае, ответ почти одинаков. Они оба должны работать как на уровне компилятора, так и на уровне процессора, так как они описывают наблюдаемое поведение программы - поэтому они должны гарантировать, что процессор не меняет все, изменяя поведение, которое они гарантируют. Но в настоящее время вы не можете написать переносимую синхронизацию потоков, потому что барьеры памяти не являются частью стандартного C ++ (поэтому они не переносимы) и volatileнедостаточно сильны, чтобы быть полезными.
Джалф
4
Пример MSDN делает это и утверждает, что инструкции не могут быть переупорядочены после нестабильного доступа: msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx
OJW
27
@OJW: Но компилятор Microsoft переопределяет volatileполный барьер памяти (предотвращая переупорядочение). Это не является частью стандарта, поэтому вы не можете полагаться на это поведение в переносимом коде.
полдень
4
@Skizz: нет, вот тут-то и возникает «волшебная часть компилятора». Барьер памяти должен понимать и процессор, и компилятор. Если компилятор понимает семантику барьера памяти, он знает, что нужно избегать подобных трюков (а также переупорядочивания операций чтения / записи через барьер). И к счастью, компилятор делает понять семантику барьер памяти, так что в конце концов, все это работает. :)
Джалф
13
@Skizz: Сами по себе потоки всегда являются зависимым от платформы расширением до C ++ 11 и C11. Насколько мне известно, каждая среда C и C ++, которая обеспечивает расширение потоков, также обеспечивает расширение "барьер памяти". Независимо от того, volatileвсегда бесполезно для многопоточного программирования. (За исключением Visual Studio, где volatile является расширением барьера памяти.)
Nemo
49

Вы можете также рассмотреть это из документации ядра Linux .

Программисты на C часто используют volatile для обозначения того, что переменная может быть изменена вне текущего потока выполнения; в результате у них иногда возникает соблазн использовать его в коде ядра, когда используются общие структуры данных. Другими словами, они, как известно, рассматривали изменчивые типы как своего рода легкую атомарную переменную, которой они не являются. Использование volatile в коде ядра почти никогда не является правильным; этот документ описывает почему.

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

Как и volatile, примитивы ядра, обеспечивающие безопасный одновременный доступ к данным (спин-блокировки, мьютексы, барьеры памяти и т. Д.), Предназначены для предотвращения нежелательной оптимизации. Если они используются должным образом, не будет необходимости использовать также и volatile. Если volatile по-прежнему необходимо, в коде почти наверняка есть ошибка. В правильно написанном коде ядра volatile может только замедлять работу.

Рассмотрим типичный блок кода ядра:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Если весь код следует правилам блокировки, значение shared_data не может неожиданно измениться, пока удерживается the_lock. Любой другой код, который может захотеть поиграть с этими данными, будет ожидать блокировки. Примитивы спин-блокировки действуют как барьеры памяти - они явно написаны для этого - это означает, что доступ к данным не будет оптимизирован для них. Таким образом, компилятор может подумать, что он знает, что будет в shared_data, но вызов spin_lock (), поскольку он действует как барьер памяти, заставит его забыть все, что он знает. Не будет проблем с оптимизацией при доступе к этим данным.

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

Класс энергозависимого хранилища изначально предназначался для регистров ввода-вывода с отображением в памяти. Внутри ядра доступ к регистрам также должен быть защищен блокировками, но также не требуется, чтобы компилятор «оптимизировал» доступ к регистрам в критической секции. Но в ядре доступ к памяти ввода / вывода всегда осуществляется через функции доступа; доступ к памяти ввода-вывода напрямую через указатели не одобряется и не работает на всех архитектурах. Эти средства доступа написаны для предотвращения нежелательной оптимизации, поэтому, опять же, volatile не нужна.

Другая ситуация, когда можно испытать желание использовать volatile, - это когда процессор занят ожиданием значения переменной. Правильный способ ожидания ожидания:

while (my_variable != what_i_want)
    cpu_relax();

Вызов cpu_relax () может снизить энергопотребление процессора или уступить многопоточному двойному процессору; оно также служит барьером памяти, поэтому, опять же, энергозависимость не нужна. Конечно, ожидание в основном это антисоциальный акт с самого начала.

Есть еще несколько редких ситуаций, когда volatile имеет смысл в ядре:

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

  • Встроенный код сборки, который изменяет память, но не имеет других видимых побочных эффектов, рискует быть удаленным GCC. Добавление ключевого слова volatile в операторы asm предотвратит это удаление.

  • Переменная jiffies является особенной в том смысле, что она может иметь различное значение при каждой ссылке на нее, но она может быть прочитана без какой-либо специальной блокировки. Таким образом, jiffies может быть изменчивым, но добавление других переменных этого типа решительно осуждается. Jiffies считается проблемой «глупого наследия» (слова Линуса) в этом отношении; исправить это будет больше проблем, чем стоит.

  • Указатели на структуры данных в когерентной памяти, которые могут быть изменены устройствами ввода-вывода, иногда могут быть законно изменчивыми. Кольцевой буфер, используемый сетевым адаптером, где этот адаптер меняет указатели, чтобы указать, какие дескрипторы были обработаны, является примером ситуации такого типа.

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

Сообщество
источник
3
@curiousguy: Да. Смотрите также gcc.gnu.org/onlinedocs/gcc-4.0.4/gcc/Extended-Asm.html .
Себастьян Мах
1
Spin_lock () выглядит как обычный вызов функции. Что особенного в этом то, что компилятор будет обрабатывать его так, чтобы сгенерированный код «забывал» любое значение shared_data, которое было прочитано до spin_lock () и сохранено в регистре, так что значение должно быть прочитано заново в do_something_on () после spin_lock ()?
Синкопированный
1
@underscore_d Я хочу сказать, что по названию функции spin_lock () я не могу сказать, что она делает что-то особенное. Я не знаю, что в этом. В частности, я не знаю, что в реализации мешает компилятору оптимизировать последующее чтение.
Синкопированный
1
Синкопированный имеет хороший момент. По сути, это означает, что программист должен знать внутреннюю реализацию этих «специальных функций» или, по крайней мере, быть очень хорошо осведомленным об их поведении. Это поднимает дополнительные вопросы, такие как: стандартизированы ли эти специальные функции и гарантированно ли они работают одинаково на всех архитектурах и на всех компиляторах? Имеется ли список таких функций или, по крайней мере, существует соглашение об использовании комментариев к коду, чтобы дать понять разработчикам, что данная функция защищает код от «оптимизации»?
JustAMartin
1
@Tuntable: приватная статика может быть затронута любым кодом через указатель. И его адрес берется. Возможно, анализ потока данных способен доказать, что указатель никогда не исчезнет, ​​но в целом это очень сложная проблема, суперлинейная по размеру программы. Если у вас есть способ гарантировать, что псевдонимы не существуют, тогда перемещение доступа через спин-блокировку должно быть в порядке. Но если псевдонимов не существует, volatileэто тоже бессмысленно. Во всех случаях поведение «вызова функции, тело которой не видно» будет правильным.
Бен Фойгт
11

Я не думаю, что вы не правы - volatile необходима, чтобы гарантировать, что поток A увидит изменение значения, если значение будет изменено чем-то другим, чем поток A. Насколько я понимаю, volatile - это в основном способ сказать компилятор «не кэшируйте эту переменную в регистре, вместо этого всегда читайте / записывайте ее из оперативной памяти при каждом доступе».

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

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

Лично мое основное (только?) Использование для флага volatile - логическое значение pleaseGoAwayNow. Если у меня есть рабочий поток, который зацикливается непрерывно, я буду проверять логическое значение volatile на каждой итерации цикла и завершать работу, если логическое значение равно true. Затем основной поток может безопасно очистить рабочий поток, установив для логического значения значение true, а затем вызвав pthread_join (), чтобы дождаться завершения рабочего потока.

Джереми Фризнер
источник
2
Ваш логический флаг, вероятно, небезопасен. Как вы гарантируете, что работник выполнит свою задачу и флаг останется в области действия до тех пор, пока он не будет прочитан (если он прочитан)? Это работа для сигналов. Volatile хорошо подходит для реализации простых спин-блокировок, если мьютекс не задействован, поскольку безопасность псевдонимов означает, что компилятор предполагает mutex_lock(и любую другую библиотечную функцию) изменить состояние переменной-флага.
Potatoswatter
6
Очевидно, это работает только в том случае, если природа подпрограммы рабочего потока такова, что она гарантированно периодически проверяет логическое значение. Флаг volatile-bool гарантированно останется в области видимости, потому что последовательность завершения потока всегда происходит до того, как объект, содержащий volatile-boolean, будет уничтожен, а последовательность завершения потока вызывает pthread_join () после установки bool. pthread_join () будет блокироваться, пока рабочий поток не исчезнет. У сигналов есть свои проблемы, особенно когда они используются в сочетании с многопоточностью.
Джереми Фризнер
2
Рабочий поток не гарантированно завершит свою работу до того, как логическое значение будет истинным - фактически, оно почти наверняка будет в середине рабочего блока, когда для bool установлено значение true. Но это не имеет значения, когда рабочий поток завершает свою рабочую единицу, потому что основной поток не будет делать ничего, кроме блокировки внутри pthread_join (), до тех пор, пока рабочий поток не завершится. Таким образом, последовательность выключения хорошо упорядочена - изменчивый bool (и любые другие общие данные) не будут освобождены до тех пор, пока не вернется pthread_join (), а pthread_join () не вернется, пока рабочий поток не исчезнет.
Джереми Фризнер
10
@ Джереми, на практике ты прав, но теоретически это все еще может сломаться. В двухъядерной системе одно ядро ​​постоянно выполняет ваш рабочий поток. Другое ядро ​​устанавливает истинное значение bool. Однако нет гарантии, что ядро ​​рабочего потока когда-либо увидит это изменение, то есть оно может никогда не остановиться, даже если оно повторяет проверку bool. Такое поведение допускается моделями памяти c ++ 0x, java и c #. На практике это никогда не произойдет, поскольку занятый поток, скорее всего, вставит куда-нибудь барьер памяти, после чего он увидит изменение в bool.
deft_code
4
Возьмите систему POSIX, используйте политику планирования в реальном времени SCHED_FIFO, более высокий статический приоритет, чем у других процессов / потоков в системе, достаточное количество ядер, должно быть вполне возможным. В Linux вы можете указать, что процесс в реальном времени может использовать 100% процессорного времени. Они никогда не будут переключаться между контекстами, если нет потока / процесса с более высоким приоритетом и никогда не будут блокироваться вводом / выводом. Но дело в том, что C / C ++ volatileне предназначен для обеспечения правильной семантики совместного использования / синхронизации данных. Я нахожу поиск особых случаев, чтобы доказать, что неправильный код может иногда работать, бесполезно.
FooF
7

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

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

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

Это не только инкапсулирует «жесткую часть», но и принципиально необходимо: C не включает атомарные операции, необходимые для реализации мьютекса; это только volatileдолжно сделать дополнительные гарантии относительно обычных операций.

Теперь у вас есть что-то вроде этого:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag не должен быть изменчивым, несмотря на то, что не кэшируется, потому что

  1. Другой поток имеет к нему доступ.
  2. Это означает, что ссылка на него должна быть когда-то взята (с &оператором).
    • (Или ссылка была взята на содержащую структуру)
  3. pthread_mutex_lock это библиотечная функция
  4. Это означает, что компилятор не может сказать, pthread_mutex_lockполучает ли каким-то образом эту ссылку.
  5. Это означает, что компилятор должен предполагать, что pthread_mutex_lockмодифицирует флаг общего доступа !
  6. Таким образом, переменная должна быть перезагружена из памяти. volatileНесмотря на то, что имеет значение в этом контексте, является посторонним.
Potatoswatter
источник
6

Ваше понимание действительно неверно.

Свойство, которое имеют изменчивые переменные, «чтение и запись в эту переменную являются частью воспринимаемого поведения программы». Это означает, что эта программа работает (при наличии соответствующего оборудования):

int volatile* reg=IO_MAPPED_REGISTER_ADDRESS;
*reg=1; // turn the fuel on
*reg=2; // ignition
*reg=3; // release
int x=*reg; // fire missiles

Проблема в том, что это не то свойство, которое мы хотим от поточно-ориентированного ничего.

Например, потокобезопасный счетчик будет просто (код, похожий на ядро ​​Linux, не знаю эквивалента c ++ 0x):

atomic_t counter;

...
atomic_inc(&counter);

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

atomic_inc(&counter);
atomic_inc(&counter);

все еще можно оптимизировать для

atomically {
  counter+=2;
}

если оптимизатор достаточно умен (он не меняет семантику кода).

jpalecek
источник
6

Чтобы ваши данные были согласованными в параллельной среде, вам необходимо выполнить два условия:

1) атомарность, т.е. если я читаю или записываю некоторые данные в память, то эти данные считываются / записываются за один проход и не могут быть прерваны или оспорены, например, из-за переключения контекста

2) Последовательность , т.е. порядка OPS чтения / записи должна быть видно , чтобы быть таким же , между несколькими параллельными средами - в том , что потоки, машина и т.д.

volatile не соответствует ни одному из вышеперечисленных - или, более конкретно, стандарт c или c ++ относительно того, как volatile должен вести себя, не включает ни одного из вышеперечисленных.

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

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

c # и java AFAIK исправляют это, заставляя volatile придерживаться 1) и 2), однако этого нельзя сказать о компиляторах c / c ++, так что в основном поступайте так, как считаете нужным.

Для более глубокого (хотя и не беспристрастного) обсуждения этой темы прочитайте это

zebrabox
источник
3
+1 - гарантированная атомность была еще одной частью того, чего мне не хватало. Я предполагал, что загрузка int является атомарной, так что энергозависимое предотвращение переупорядочения обеспечило полное решение на стороне чтения. Я думаю, что это приемлемое допущение для большинства архитектур, но это не гарантия.
Майкл Экстранд
Когда человек читает и пишет в память, прерываемый и не атомарный? Есть ли какая-то выгода?
Батбрат
5

В FAQ по comp.programming.threads есть классическое объяснение Дейва Бутенхофа:

В56: Почему мне не нужно объявлять общие переменные VOLATILE?

Однако меня беспокоят случаи, когда и компилятор, и библиотека потоков выполняют свои соответствующие спецификации. Соответствующий компилятор C может глобально распределить некоторую разделяемую (энергонезависимую) переменную в регистр, который сохраняется и восстанавливается при передаче ЦП из потока в поток. Каждый поток будет иметь свое личное значение для этой общей переменной, а это не то, что мы хотим от общей переменной.

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

Так что да, это правда, что компилятор, который строго (но очень агрессивно) соответствует ANSI C, может не работать с несколькими потоками без volatile. Но кому-то лучше это исправить. Потому что любая СИСТЕМА (то есть, прагматически, комбинация ядра, библиотек и компилятора C), которая не обеспечивает гарантии согласованности памяти POSIX, не СООТВЕТСТВУЕТ стандарту POSIX. Период. Система НЕ МОЖЕТ требовать, чтобы вы использовали volatile с общими переменными для правильного поведения, потому что POSIX требует только того, чтобы были необходимы функции синхронизации POSIX.

Так что, если ваша программа не работает, потому что вы не использовали volatile, это БАГ. Это может быть не ошибка в C, или ошибка в библиотеке потоков, или ошибка в ядре. Но это системная ошибка, и один или несколько из этих компонентов должны будут исправиться.

Вы не хотите использовать volatile, потому что в любой системе, где это имеет значение, это будет значительно дороже, чем правильная энергонезависимая переменная. (ANSI C требует «точек последовательности» для изменчивых переменных в каждом выражении, в то время как POSIX требует их только при операциях синхронизации - многопоточное приложение с интенсивными вычислениями будет видеть значительно большую активность памяти, используя volatile, и, в конце концов, именно активность памяти действительно тормозит тебя.)

/ --- [Дейв Бутенхоф] ----------------------- [butenhof@zko.dec.com] --- \
| Корпорация цифрового оборудования 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, ФАКС 603.881.0120 Nashua NH 03062-2698 |
----------------- [Лучше жить через параллелизм] ---------------- /

Г-н Бутенхоф охватывает большую часть того же вопроса в этом посте usenet :

Использование «volatile» недостаточно для обеспечения надлежащей видимости памяти или синхронизации между потоками. Использование мьютекса является достаточным, и, за исключением использования различных альтернатив непереносимого машинного кода (или более тонких последствий правил памяти POSIX, которые в целом гораздо сложнее применять, как объяснялось в моем предыдущем посте), НЕОБХОДИМО мьютекс.

Поэтому, как объяснил Брайан, использование volatile не дает ничего, кроме того, что мешает компилятору делать полезные и желательные оптимизации, не оказывая никакой помощи в обеспечении безопасности кода. Разумеется, вы можете объявить все, что вы хотите, «изменчивыми» - в конце концов, это законный атрибут хранения ANSI C. Только не ожидайте, что это решит любые проблемы синхронизации потока для Вас.

Все это в равной степени применимо к C ++.

Тони Делрой
источник
Ссылка не работает; похоже, это больше не указывает на то, что вы хотели процитировать. Без текста это своего рода бессмысленный ответ.
jww
3

Это все, что делает «volatile»: «Привет, компилятор, эта переменная может измениться в ЛЮБОЙ МОМЕНТ (при любом такте), даже если на него не действуют НЕТ ЛОКАЛЬНЫХ ИНСТРУКЦИЙ. НЕ кэшируйте это значение в регистре».

Это ЭТО. Он сообщает компилятору, что ваше значение является изменчивым - это значение может быть изменено в любой момент внешней логикой (другой поток, другой процесс, ядро ​​и т. Д.). Он существует более или менее исключительно для подавления оптимизаций компилятора, которые будут автоматически кэшировать значение в регистре, которое по своей природе небезопасно для КЕШЕГО кэша.

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

Зак Йезек
источник
3

Согласно моему старому стандарту C, «то, что представляет собой доступ к объекту, имеющему тип, определяемый volatile, определяется реализацией» . Таким образом, авторы компилятора C могли выбрать «изменчивый», означающий «потокобезопасный доступ в многопроцессорной среде» . Но они этого не сделали.

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

Это означает, что такие вещи, как «изменчивые» семафоры вокруг критических разделов кода, которые не работают на новом оборудовании с новыми компиляторами, могли когда-то работать со старыми компиляторами на старом оборудовании, и старые примеры иногда не ошибаются, а просто старые.

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