Как показано в этом ответе, который я недавно опубликовал, мне кажется, что я не совсем понимаю полезность (или ее отсутствие) volatile
многопоточного программирования.
Насколько я понимаю, в любой момент, когда переменная может быть изменена вне потока контроля фрагмента кода, обращающегося к ней, эта переменная должна быть объявлена volatile
. Обработчики сигналов, регистры ввода / вывода и переменные, измененные другим потоком, представляют собой такие ситуации.
Итак, если у вас есть глобальный int foo
, и foo
он читается одним потоком и атомарно устанавливается другим потоком (возможно, с использованием соответствующей машинной инструкции), поток чтения видит эту ситуацию так же, как он видит переменную, настроенную обработчиком сигнала или изменено внешним аппаратным условием и, таким образом, foo
должно быть объявлено volatile
(или, для многопоточных ситуаций, доступ к которому осуществляется с нагрузкой из памяти, что, вероятно, является лучшим решением)
Как и где я не прав?
Ответы:
Проблема
volatile
в многопоточном контексте заключается в том, что он не обеспечивает всех необходимых нам гарантий. У него есть несколько необходимых нам свойств, но не все, поэтому мы не можем полагатьсяvolatile
только на них .Тем не менее, примитивы, которые мы должны использовать для оставшихся свойств, также предоставляют те, которые
volatile
делают, так что это фактически не нужно.Для поточно-ориентированного доступа к общим данным нам нужна гарантия того, что:
volatile
переменную в качестве флага, чтобы указать, готовы ли некоторые данные для чтения. В нашем коде мы просто устанавливаем флаг после подготовки данных, поэтому все выглядит хорошо. Но что, если инструкции переупорядочены так, чтобы флаг был установлен первым ?volatile
действительно гарантирует первый пункт. Это также гарантирует отсутствие переупорядочения между различными изменчивыми операциями чтения / записи . Всеvolatile
обращения к памяти будут происходить в том порядке, в котором они указаны. Это все, что нам нужно для того, для чегоvolatile
предназначено: манипулирование регистрами ввода-вывода или отображенным в память оборудованием, но это не помогает нам в многопоточном коде, гдеvolatile
объект часто используется только для синхронизации доступа к энергонезависимым данным. Эти доступы все еще могут быть переупорядочены относительно тех,volatile
которые доступны .Решением для предотвращения переупорядочения является использование барьера памяти , который указывает и для компилятора, и для ЦП, что доступ к памяти не может быть переупорядочен через эту точку . Размещение таких барьеров вокруг нашего энергозависимого доступа к переменным гарантирует, что даже энергонезависимый доступ не будет переупорядочен через энергозависимый, что позволяет нам писать поточно-ориентированный код.
Тем не менее, барьеры памяти также гарантируют, что все ожидающие чтения / записи выполняются, когда барьер достигнут, так что он фактически дает нам все, что нам нужно, сам по себе, делая
volatile
ненужным. Мы можем просто полностью удалитьvolatile
классификатор.Начиная с C ++ 11, атомарные переменные (
std::atomic<T>
) дают нам все соответствующие гарантии.источник
volatile
недостаточно сильны, чтобы быть полезными.volatile
полный барьер памяти (предотвращая переупорядочение). Это не является частью стандарта, поэтому вы не можете полагаться на это поведение в переносимом коде.volatile
всегда бесполезно для многопоточного программирования. (За исключением Visual Studio, где volatile является расширением барьера памяти.)Вы можете также рассмотреть это из документации ядра Linux .
источник
volatile
это тоже бессмысленно. Во всех случаях поведение «вызова функции, тело которой не видно» будет правильным.Я не думаю, что вы не правы - volatile необходима, чтобы гарантировать, что поток A увидит изменение значения, если значение будет изменено чем-то другим, чем поток A. Насколько я понимаю, volatile - это в основном способ сказать компилятор «не кэшируйте эту переменную в регистре, вместо этого всегда читайте / записывайте ее из оперативной памяти при каждом доступе».
Путаница в том, что изменчивости недостаточно для реализации ряда вещей. В частности, современные системы используют многоуровневое кэширование, современные многоядерные процессоры выполняют некоторые модные оптимизации во время выполнения, а современные компиляторы делают некоторые модные оптимизации во время компиляции, и все это может привести к различным побочным эффектам, проявляющимся в разных порядок из порядка, который вы ожидаете, если вы просто посмотрите на исходный код.
Такая изменчивость - это хорошо, если вы помните, что «наблюдаемые» изменения в изменчивой переменной могут не произойти точно в то время, когда вы думаете, что это произойдет. В частности, не пытайтесь использовать переменные переменные как способ синхронизации или упорядочения операций между потоками, потому что это не будет работать надежно.
Лично мое основное (только?) Использование для флага volatile - логическое значение pleaseGoAwayNow. Если у меня есть рабочий поток, который зацикливается непрерывно, я буду проверять логическое значение volatile на каждой итерации цикла и завершать работу, если логическое значение равно true. Затем основной поток может безопасно очистить рабочий поток, установив для логического значения значение true, а затем вызвав pthread_join (), чтобы дождаться завершения рабочего потока.
источник
mutex_lock
(и любую другую библиотечную функцию) изменить состояние переменной-флага.SCHED_FIFO
, более высокий статический приоритет, чем у других процессов / потоков в системе, достаточное количество ядер, должно быть вполне возможным. В Linux вы можете указать, что процесс в реальном времени может использовать 100% процессорного времени. Они никогда не будут переключаться между контекстами, если нет потока / процесса с более высоким приоритетом и никогда не будут блокироваться вводом / выводом. Но дело в том, что C / C ++volatile
не предназначен для обеспечения правильной семантики совместного использования / синхронизации данных. Я нахожу поиск особых случаев, чтобы доказать, что неправильный код может иногда работать, бесполезно.volatile
полезен (хотя и недостаточен) для реализации базовой конструкции мьютекса спин-блокировки, но если у вас есть это (или что-то превосходное), вам не нужен другойvolatile
.Типичный способ многопоточного программирования - не защищать каждую общую переменную на уровне машины, а вводить защитные переменные, которые управляют ходом программы. Вместо
volatile bool my_shared_flag;
вас должно бытьЭто не только инкапсулирует «жесткую часть», но и принципиально необходимо: C не включает атомарные операции, необходимые для реализации мьютекса; это только
volatile
должно сделать дополнительные гарантии относительно обычных операций.Теперь у вас есть что-то вроде этого:
my_shared_flag
не должен быть изменчивым, несмотря на то, что не кэшируется, потому что&
оператором).pthread_mutex_lock
это библиотечная функцияpthread_mutex_lock
получает ли каким-то образом эту ссылку.pthread_mutex_lock
модифицирует флаг общего доступа !volatile
Несмотря на то, что имеет значение в этом контексте, является посторонним.источник
Ваше понимание действительно неверно.
Свойство, которое имеют изменчивые переменные, «чтение и запись в эту переменную являются частью воспринимаемого поведения программы». Это означает, что эта программа работает (при наличии соответствующего оборудования):
Проблема в том, что это не то свойство, которое мы хотим от поточно-ориентированного ничего.
Например, потокобезопасный счетчик будет просто (код, похожий на ядро Linux, не знаю эквивалента c ++ 0x):
Это атомно, без барьера памяти. Вы должны добавить их при необходимости. Добавление volatile, вероятно, не поможет, поскольку оно не будет связывать доступ к соседнему коду (например, к добавлению элемента в список, который считает счетчик). Конечно, вам не нужно видеть счетчик увеличенным вне вашей программы, и оптимизация все еще желательна, например.
все еще можно оптимизировать для
если оптимизатор достаточно умен (он не меняет семантику кода).
источник
Чтобы ваши данные были согласованными в параллельной среде, вам необходимо выполнить два условия:
1) атомарность, т.е. если я читаю или записываю некоторые данные в память, то эти данные считываются / записываются за один проход и не могут быть прерваны или оспорены, например, из-за переключения контекста
2) Последовательность , т.е. порядка OPS чтения / записи должна быть видно , чтобы быть таким же , между несколькими параллельными средами - в том , что потоки, машина и т.д.
volatile не соответствует ни одному из вышеперечисленных - или, более конкретно, стандарт c или c ++ относительно того, как volatile должен вести себя, не включает ни одного из вышеперечисленных.
На практике это даже хуже, так как некоторые компиляторы (такие как компилятор Intel Itanium) пытаются реализовать некоторый элемент безопасного поведения при одновременном доступе (т. Е. Путем обеспечения ограничений памяти), однако между реализациями компилятора нет согласованности, и, кроме того, стандарт не требует этого реализации в первую очередь.
Если пометить переменную как volatile, это будет означать, что вы заставляете значение сбрасываться в память и из памяти каждый раз, что во многих случаях просто замедляет ваш код, поскольку вы в основном снижаете производительность своего кэша.
c # и java AFAIK исправляют это, заставляя volatile придерживаться 1) и 2), однако этого нельзя сказать о компиляторах c / c ++, так что в основном поступайте так, как считаете нужным.
Для более глубокого (хотя и не беспристрастного) обсуждения этой темы прочитайте это
источник
В FAQ по comp.programming.threads есть классическое объяснение Дейва Бутенхофа:
Г-н Бутенхоф охватывает большую часть того же вопроса в этом посте usenet :
Все это в равной степени применимо к C ++.
источник
Это все, что делает «volatile»: «Привет, компилятор, эта переменная может измениться в ЛЮБОЙ МОМЕНТ (при любом такте), даже если на него не действуют НЕТ ЛОКАЛЬНЫХ ИНСТРУКЦИЙ. НЕ кэшируйте это значение в регистре».
Это ЭТО. Он сообщает компилятору, что ваше значение является изменчивым - это значение может быть изменено в любой момент внешней логикой (другой поток, другой процесс, ядро и т. Д.). Он существует более или менее исключительно для подавления оптимизаций компилятора, которые будут автоматически кэшировать значение в регистре, которое по своей природе небезопасно для КЕШЕГО кэша.
Вы можете столкнуться с такими статьями, как «Доктор Доббс», которые представляют собой нестабильную панацею для многопоточного программирования. Его подход не полностью лишен достоинств, но он имеет фундаментальный недостаток, заключающийся в том, что пользователи объекта несут ответственность за его безопасность потоков, что приводит к тем же проблемам, что и другие нарушения инкапсуляции.
источник
Согласно моему старому стандарту C, «то, что представляет собой доступ к объекту, имеющему тип, определяемый volatile, определяется реализацией» . Таким образом, авторы компилятора C могли выбрать «изменчивый», означающий «потокобезопасный доступ в многопроцессорной среде» . Но они этого не сделали.
Вместо этого в качестве новых функций, определяемых реализацией, были добавлены операции, необходимые для обеспечения безопасности потока критической секции в многоядерной многопроцессорной среде с общей памятью. И, освободившись от требования, что «volatile» будет обеспечивать атомарный доступ и упорядочение доступа в многопроцессорной среде, разработчики компиляторов расставили приоритеты сокращения кода над исторически зависимой от реализации «изменчивой» семантикой.
Это означает, что такие вещи, как «изменчивые» семафоры вокруг критических разделов кода, которые не работают на новом оборудовании с новыми компиляторами, могли когда-то работать со старыми компиляторами на старом оборудовании, и старые примеры иногда не ошибаются, а просто старые.
источник
volatile
нужно сделать, чтобы позволить писать операционную систему способом, который зависит от оборудования, но не зависит от компилятора. Требование, чтобы программисты использовали функции, зависящие от реализации, а не заставлялиvolatile
работать так, как требуется, подрывали цель наличия стандарта.