Почему volatile не считается полезным в многопоточном программировании на C или C ++? - PullRequest
155 голосов
/ 21 марта 2010

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

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

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

Как и где я ошибаюсь?

Ответы [ 9 ]

205 голосов
/ 21 марта 2010

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

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

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

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

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

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

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

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

47 голосов
/ 21 марта 2010

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

C программисты часто воспринимают переменные как означающие, что переменная может быть изменено за пределами текущего потока выполнения; как В результате они иногда испытывают желание использовать его в коде ядра, когда общие структуры данных используются. Другими словами, они были известно, что летучие типы рассматриваются как своего рода легкая атомарная переменная, которая они не. Использование 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 в критическом разделе, когда мы знаем, что никто другой не может работать с этим. Пока блокировка удерживается, Общие_данные не являются энергозависимыми. При работе с общими данными, собственно блокировка делает энергозависимые ненужными и потенциально опасными.

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

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

while (my_variable != what_i_want)
    cpu_relax();

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

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

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

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

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

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

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

11 голосов
/ 21 марта 2010

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

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

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

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

6 голосов
/ 21 марта 2010

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

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

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

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

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

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

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

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

6 голосов
/ 21 марта 2010

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 не нужноvolatile, несмотря на то, что он не кэшируется, потому что

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

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

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

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

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

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

atomic_t counter;

...
atomic_inc(&counter);

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

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

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

atomically {
  counter+=2;
}

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

5 голосов
/ 05 октября 2010

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

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

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

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

Так что да, это правда, что компилятор, который строго соответствует (но очень агрессивно), чтобы ANSI C не мог работать с несколькими потоками без неустойчивый. Но кому-то лучше это исправить. Потому что любая СИСТЕМА (то есть прагматически, комбинация ядра, библиотек и компилятора 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 ++.

3 голосов
/ 14 ноября 2014

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

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

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

2 голосов
/ 02 августа 2014

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

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

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

...