Если есть два потока, обращающихся к глобальной переменной, то во многих руководствах говорится, что переменная должна быть изменчивой, чтобы компилятор не кэшировал переменную в регистре и, таким образом, не обновлялся правильно. Однако два потока, оба обращающиеся к общей переменной, - это то, что требует защиты через мьютекс, не так ли? Но в этом случае между блокировкой потока и освобождением мьютекса код находится в критическом разделе, где только один поток может получить доступ к переменной, и в этом случае переменная не должна быть изменчивой?
Итак, каково использование / цель volatile в многопоточной программе?
c++
multithreading
concurrency
atomic
volatile
Дэвид Престон
источник
источник
Ответы:
Короткий и быстрый ответ :
volatile
(почти) бесполезен для независимого от платформы программирования многопоточных приложений. Он не обеспечивает никакой синхронизации, он не создает заборов памяти и не обеспечивает порядок выполнения операций. Это не делает операции атомарными. Это не делает ваш код волшебным образом потокобезопасным.volatile
может быть самым непонятым средством во всем C ++. Смотрите это , это и это для получения дополнительной информации оvolatile
С другой стороны, у
volatile
него есть некоторая польза, которая может быть не такой очевидной. Его можно использовать почти так же, как если бы онconst
помог компилятору показать вам, где вы могли совершить ошибку при доступе к некоторому общему ресурсу незащищенным способом. Это использование обсуждается Александреску в этой статье. . Однако это в основном использование системы типов C ++ таким образом, который часто рассматривается как выдумка и может вызвать неопределенное поведение.volatile
был специально предназначен для использования при взаимодействии с отображаемым в память оборудованием, обработчиками сигналов и инструкцией машинного кода setjmp. Это делает егоvolatile
непосредственно применимым к программированию на системном уровне, а не к нормальному программированию на уровне приложений.В стандарте C ++ 2003 не говорится, что
volatile
к переменным применяется какая-либо семантика Acquire или Release. Фактически, Стандарт полностью ничего не говорит о многопоточности. Однако на определенных платформах семантика получения и выпуска применяется кvolatile
переменным.[Обновление для C ++ 11]
C ++ 11 Стандарта в настоящее время делает квитирование многопоточности непосредственно в модели памяти и lanuage и предоставляет библиотечные средства для борьбы с ним в кроссплатформенном пути. Однако семантика до
volatile
сих пор не изменилась.volatile
до сих пор не является механизмом синхронизации. Бьярн Страуструп говорит об этом в TCPPPL4E:[/ Конец обновления]
Все вышесказанное относится к самому языку C ++, как определено стандартом 2003 г. (а теперь и стандартом 2011 г.). Однако некоторые конкретные платформы добавляют дополнительные функции или ограничения к тому, что они
volatile
делают. Так , например, в MSVC 2010 (по крайней мере) Приобретать и Release Семантика действительно относятся к определенным операциям поvolatile
переменным. Из MSDN :Тем не менее, вы можете принять к сведению тот факт , что если вы будете следовать приведенной выше ссылке, есть некоторые дебаты в комментариях по поводу того, не приобретать / отпускание семантика фактически применяется в данном случае.
источник
volatile
нее, это потому, что вы стояли на плечах людей, которые раньшеvolatile
реализовывали библиотеки потоков.volatile
самом деле делает в C ++. То, что сказал @John, правильно , конец истории. Это не имеет ничего общего с кодом приложения и библиотечным кодом или «обычными» и «богоподобными всеведущими программистами» в этом отношении.volatile
не нужен и бесполезен для синхронизации между потоками. Библиотеки потоков не могут быть реализованы в терминахvolatile
; он в любом случае должен полагаться на детали, специфичные для платформы, и когда вы полагаетесь на них, они вам больше не нужныvolatile
.(Примечание редактора: C ++ 11
volatile
не является подходящим инструментом для этой работы и все еще имеет UB гонки данных. Используйтеstd::atomic<bool>
сstd::memory_order_relaxed
load / store, чтобы сделать это без UB. В реальных реализациях он будет компилироваться в тот же asm, что иvolatile
. Я добавил ответ с более подробной информацией, а также устранение заблуждений в комментариях о том, что слабоупорядоченная память может быть проблемой для этого варианта использования: все реальные процессоры имеют согласованную общую память, поэтомуvolatile
будут работать для этого в реальных реализациях C ++. Но все же не не делай этого.Некоторое обсуждение в комментариях , кажется, говорить о других потребительных случаях , когда вам будет необходимо что - то более сильное , чем расслабленных Атомикс. Этот ответ уже указывает на то, что
volatile
вам не нужно упорядочивать.)Volatile иногда бывает полезным по следующей причине: этот код:
оптимизирован gcc для:
Что явно неверно, если флаг записывается другим потоком. Обратите внимание, что без этой оптимизации механизм синхронизации, вероятно, будет работать (в зависимости от другого кода могут потребоваться некоторые барьеры памяти) - нет необходимости в мьютексе в сценарии 1 производитель - 1 потребитель.
В противном случае ключевое слово volatile слишком странно, чтобы его можно было использовать - оно не обеспечивает никаких гарантий упорядочения памяти как для энергозависимого, так и для энергонезависимого доступа и не предоставляет никаких атомарных операций - то есть вы не получите помощи от компилятора с ключевым словом volatile, кроме отключенного кэширования регистров ,
источник
volatile
не предотвращает изменение порядка доступа к памяти.volatile
доступы не будут переупорядочены относительно друг друга, но они не дают никаких гарантий относительно переупорядочения относительно неvolatile
объектов, и поэтому они в основном бесполезны в качестве флагов.volatile
.while (work_left) { do_piece_of_work(); if (cancel) break;}
если отмена переупорядочивается в цикле, логика все еще в силе.У меня был фрагмент кода, который работал аналогично: если основной поток хочет завершить работу, он устанавливает флаг для других потоков, но это не так ...В C ++ 11 обычно никогда не используйте
volatile
для потоковой передачи, только для MMIOНо TL: DR, он действительно "работает" как атомарный
mo_relaxed
на оборудовании с согласованными кэшами (то есть со всем); достаточно, чтобы компиляторы не сохраняли вары в регистрах.atomic
не нужны барьеры памяти для создания атомарности или видимости между потоками, только для того, чтобы текущий поток ожидал до / после операции, чтобы создать порядок между доступами этого потока к различным переменным.mo_relaxed
никогда не нуждается в каких-либо барьерах, просто загружайте, храните или RMW.Для рулонного своего собственного Атомикса с
volatile
(и инлайн-ассемблер для барьеров) в старые времена до C ++ 11std::atomic
,volatile
был только хорошим способом получить некоторые вещи для работы . Но это зависело от множества предположений о том, как работают реализации, и никогда не гарантировалось никакими стандартами.Например, ядро Linux по-прежнему использует собственный атомарный аппарат с ручным управлением.
volatile
, но поддерживает только несколько конкретных реализаций C (GNU C, clang и, возможно, ICC). Частично это связано с расширениями GNU C и встроенным синтаксисом и семантикой asm, но также потому, что это зависит от некоторых предположений о том, как работают компиляторы.Для новых проектов это почти всегда неправильный выбор; вы можете использовать
std::atomic
(withstd::memory_order_relaxed
), чтобы компилятор генерировал такой же эффективный машинный код, как и выvolatile
.std::atomic
сmo_relaxed
устаревшимиvolatile
для потоковой передачи. (за исключением, возможно, работы с ошибками упущенной оптимизацииatomic<double>
в некоторых компиляторах .)Внутренняя реализация
std::atomic
основных компиляторов (таких как gcc и clang) используется не только дляvolatile
внутренних целей ; компиляторы напрямую предоставляют атомарные функции загрузки, хранения и встроенные функции RMW. (например, встроенные функции GNU C,__atomic
которые работают с "простыми" объектами.)Volatile можно использовать на практике (но не делайте этого)
Тем не менее, он
volatile
может использоваться на практике для таких вещей, какexit_now
флаг на всех (?) Существующих реализациях C ++ на реальных процессорах, из-за того, как работают процессоры (согласованные кеши) и общих предположений о том, какvolatile
должны работать. Но больше нечего и не рекомендуется. Цель этого ответа - объяснить, как на самом деле работают существующие процессоры и реализации C ++. Если вас это не волнует, все, что вам нужно знать, это то, чтоstd::atomic
mo_relaxed устарелvolatile
для многопоточности.(Стандарт ISO C ++ об этом довольно расплывчатый, просто говорится, что
volatile
доступы должны оцениваться строго в соответствии с правилами абстрактной машины C ++, а не оптимизироваться. Учитывая, что реальные реализации используют адресное пространство памяти машины для моделирования адресного пространства C ++, это означает, чтоvolatile
операции чтения и присваивания должны компилироваться для загрузки / сохранения инструкций для доступа к объектному представлению в памяти.)Как указывает другой ответ,
exit_now
флаг - это простой случай межпотокового взаимодействия, который не требует никакой синхронизации : он не публикует, что содержимое массива готово или что-то в этом роде. Просто магазин, который сразу замечается неоптимизированной загрузкой в другом потоке.Без volatile или atomic правило as-if и предположение об отсутствии UB-гонки данных позволяет компилятору оптимизировать его в asm, который проверяет флаг только один раз , прежде чем войти (или нет) в бесконечный цикл. Именно это и происходит в реальной жизни с настоящими компиляторами. (И обычно оптимизируют большую часть
do_stuff
из-за того, что цикл никогда не завершается, поэтому любой последующий код, который мог бы использовать результат, недоступен, если мы войдем в цикл).Многопоточная программа застряла в оптимизированном режиме, но нормально работает в -O0 - это пример (с описанием вывода asm GCC) того, как именно это происходит с GCC на x86-64. Также программирование MCU - оптимизация C ++ O2 прерывается, а цикл на электронике. SE показывает другой пример.
Обычно нам нужна агрессивная оптимизация, которая позволяет CSE и поднимать нагрузки за пределы петель, в том числе для глобальных переменных.
До C ++ 11 это
volatile bool exit_now
был один из способов заставить эту работу работать должным образом (в обычных реализациях C ++). Но в C ++ 11 UB-гонка данных по-прежнему применяется,volatile
поэтому стандарт ISO на самом деле не гарантирует , что он будет работать везде, даже при условии согласованного кеширования HW.Обратите внимание, что для более широких типов это
volatile
не гарантирует отсутствия разрывов. Я проигнорировал это различие здесь,bool
потому что это не проблема для обычных реализаций. Но это также одна из причин, почемуvolatile
все еще подвержен гонке данных UB вместо того, чтобы быть эквивалентом расслабленного атомарного.Обратите внимание, что «как задумано» не означает, что выполняющий поток
exit_now
ожидает фактического завершения другого потока. Или даже то, что он ждет, пока изменчивоеexit_now=true
хранилище даже не станет глобально видимым, прежде чем продолжить последующие операции в этом потоке. (atomic<bool>
со значением по умолчанию онmo_seq_cst
будет ждать, по крайней мере, до любой последующей загрузки seq_cst. На многих ISA вы просто получите полный барьер после store).C ++ 11 предоставляет способ, отличный от UB, который компилирует то же самое
Флаг "продолжить работу" или "выйти сейчас" следует использовать
std::atomic<bool> flag
сmo_relaxed
С помощью
flag.store(true, std::memory_order_relaxed)
while( !flag.load(std::memory_order_relaxed) ) { ... }
предоставит вам тот же самый asm (без дорогостоящих инструкций по барьерам), который вы бы получили
volatile flag
.Помимо отсутствия разрыва, он
atomic
также дает вам возможность хранить в одном потоке и загружать в другом без UB, поэтому компилятор не может поднять нагрузку из цикла. (Предположение об отсутствии UB-гонки данных - это то, что позволяет проводить агрессивную оптимизацию, которую мы хотим для неатомарных энергонезависимых объектов.) Эта функцияatomic<T>
почти такая же, какvolatile
и для чистых загрузок и чистых хранилищ.atomic<T>
также make+=
и т. д. в атомарных RMW-операциях (значительно дороже, чем атомарная загрузка во временное, операционное, затем отдельное атомарное хранилище. Если вам не нужен атомарный RMW, напишите свой код с локальным временным хранилищем).С
seq_cst
порядком по умолчанию, который вы бы получилиwhile(!flag)
, он также добавляет гарантии заказа по отношению к. неатомарные обращения и другие атомарные обращения.(Теоретически стандарт ISO C ++ не исключает оптимизацию атомики во время компиляции. Но на практике компиляторы этого не делают, потому что нет способа контролировать, когда это будет плохо. Есть несколько случаев, когда даже
volatile atomic<T>
не может иметь достаточный контроль над оптимизацией атомики, если компиляторы оптимизировали, поэтому пока компиляторы этого не делают. См. Почему компиляторы не объединяют избыточные записи std :: atomic? Обратите внимание, что wg21 / p0062 не рекомендует использоватьvolatile atomic
в текущем коде для защиты от оптимизации атомарный.)volatile
действительно работает для этого на реальных процессорах (но все же не используйте его)даже со слабоупорядоченными моделями памяти (не x86) . Но на самом деле не использовать его, использовать
atomic<T>
сmo_relaxed
вместо !! Целью этого раздела является устранение неправильных представлений о том, как работают настоящие процессоры, а не оправданиеvolatile
. Если вы пишете код без блокировки, вы, вероятно, заботитесь о производительности. Понимание кешей и затрат на межпотоковое взаимодействие обычно важно для хорошей производительности.Реальные процессоры имеют согласованные кеши / разделяемую память: после того, как хранилище одного ядра становится глобально видимым, никакое другое ядро не может загрузить устаревшее значение. (См. Также Мифы, которые верят программистам о кэшах ЦП, в котором рассказывается о нестабильности Java, эквивалентной C ++
atomic<T>
с порядком памяти seq_cst.)Когда я говорю « загрузка» , я имею в виду инструкцию asm, которая обращается к памяти. Это то, что
volatile
обеспечивает доступ, и это не то же самое, что преобразование lvalue-to-rvalue неатомарной / энергонезависимой переменной C ++. (например,local_tmp = flag
илиwhile(!flag)
).Единственное, что вам нужно победить, - это оптимизации времени компиляции, которые вообще не перезагружаются после первой проверки. Достаточно любой нагрузки + проверки на каждой итерации, без упорядочивания. Без синхронизации между этим потоком и основным потоком не имеет смысла говорить о том, когда именно произошло хранилище или порядок загрузки wrt. другие операции в цикле. Только тогда, когда он виден этой теме, имеет значение. Когда вы видите установленный флаг exit_now, вы выходите. Межъядерная задержка на типичном x86 Xeon может составлять примерно 40 нс между отдельными физическими ядрами .
Теоретически: потоки C ++ на оборудовании без согласованных кешей
Я не вижу никакого способа, которым это могло бы быть удаленно эффективным, используя только чистый ISO C ++, не требуя от программиста явного сброса исходного кода.
Теоретически у вас может быть реализация C ++ на машине, которая не похожа на эту, требуя генерируемых компилятором явных сбросов, чтобы сделать вещи видимыми для других потоков на других ядрах . (Или для чтения, чтобы не использовать возможно устаревшую копию). Стандарт C ++ не делает это невозможным, но модель памяти C ++ спроектирована так, чтобы быть эффективной на машинах с согласованной общей памятью. Например, стандарт C ++ даже говорит о «согласованности чтения-чтения», «согласованности чтения-записи» и т. Д. Одно примечание в стандарте даже указывает на связь с оборудованием:
Нет никакого механизма, чтобы
release
хранилище только очищало себя и несколько выбранных диапазонов адресов: ему пришлось бы синхронизировать все, потому что он не знал бы, что другие потоки могли бы захотеть прочитать, если бы их загрузка-загрузка увидела это хранилище релизов (формируя Release-sequence, которая устанавливает связь между потоками «происходит до», гарантируя, что более ранние неатомарные операции, выполненные потоком записи, теперь безопасны для чтения. Если только он не выполняет дальнейшую запись в них после хранилища релизов ...) Или компиляторы будут иметь быть действительно умным, чтобы доказать, что только несколько строк кэша нуждаются в очистке.Связанный: мой ответ на вопрос "Безопасно ли mov + mfence на NUMA"? подробно рассказывает об отсутствии систем x86 без согласованной разделяемой памяти. Также связано: переупорядочивание загрузок и хранилищ на ARM для получения дополнительной информации о загрузках / хранилищах в том же месте.
Там являются Я думаю , что кластеры с некогерентного общей памяти, но они не одной системы изображения машины. Каждый домен когерентности запускает отдельное ядро, поэтому вы не можете запускать потоки одной программы C ++ через него. Вместо этого вы запускаете отдельные экземпляры программы (каждый со своим адресным пространством: указатели в одном экземпляре недействительны в другом).
Чтобы заставить их взаимодействовать друг с другом посредством явного сброса, вы обычно используете MPI или другой API передачи сообщений, чтобы программа указала, какие диапазоны адресов нуждаются в сбросе.
Настоящее оборудование не
std::thread
выходит за рамки согласованности кеша:Существуют некоторые асимметричные чипы ARM с общим физическим адресным пространством, но без внутренних общих кеш-доменов. Так что не связно. (например, комментарии к ядру A8 и Cortex-M3, например TI Sitara AM335x).
Но на этих ядрах будут работать разные ядра, а не единый образ системы, который мог бы запускать потоки на обоих ядрах. Я не знаю никаких реализаций C ++, которые запускают
std::thread
потоки через ядра ЦП без согласованных кешей.В частности, для ARM GCC и clang генерируют код, предполагая, что все потоки выполняются в одном внутреннем разделяемом домене. Фактически, в руководстве ARMv7 ISA сказано:
Таким образом, некогерентная разделяемая память между отдельными доменами - это только вещь для явного специфичного для системы использования областей разделяемой памяти для связи между различными процессами под разными ядрами.
См. Также это обсуждение CoreCLR о создании кода, использующем
dmb ish
(Внутренний разделяемый барьер) иdmb sy
(Системные) барьеры памяти в этом компиляторе.Я утверждаю, что никакая реализация C ++ для других ISA не работает
std::thread
через ядра с некогерентными кешами. У меня нет доказательств того, что такой реализации не существует, но это кажется маловероятным. Если вы не нацеливаетесь на конкретную экзотическую часть HW, которая работает таким образом, ваши размышления о производительности должны предполагать MESI-подобную когерентность кеша между всеми потоками. (Однако желательно использоватьatomic<T>
способы, гарантирующие правильность!)Согласованные кеши упрощают
Но в многоядерной системе с согласованными кэшами реализация хранилища релизов просто означает упорядочивание фиксации в кеше для хранилищ этого потока, а не выполнение какой-либо явной очистки. ( https://preshing.com/20120913/acquire-and-release-semantics/ и https://preshing.com/20120710/memory-barriers-are-like-source-control-operations/ ). (А загрузка-получение означает упорядочение доступа к кешу в другом ядре).
Команда барьера памяти просто блокирует загрузку и / или сохранение текущего потока до тех пор, пока буфер хранения не иссякнет; это всегда происходит как можно быстрее само по себе. ( Обеспечивает ли барьер памяти целостность кэша? Устраняет это заблуждение). Так что, если вам не нужен заказ, просто укажите видимость в других потоках,
mo_relaxed
это нормально. (И так и естьvolatile
, но не делайте этого.)См. Также сопоставления C / C ++ 11 с процессорами
Интересный факт: на x86 каждое хранилище asm является хранилищем выпуска, потому что модель памяти x86 в основном представляет собой seq-cst плюс буфер хранилища (с пересылкой хранилища).
Наполовину связанный буфер re: store, глобальная видимость и согласованность: C ++ 11 гарантирует очень мало. Большинство реальных ISA (кроме PowerPC) действительно гарантируют, что все потоки могут согласовать порядок появления двух хранилищ двумя другими потоками. (В формальной терминологии модели памяти компьютерной архитектуры они называются «атомарными с множеством копий»).
Другое заблуждение состоит в том, что инструкции asm с ограничением памяти необходимы для очистки буфера хранилища, чтобы другие ядра вообще могли видеть наши хранилища . На самом деле буфер хранилища всегда пытается опустошить себя (зафиксировать кеш L1d) как можно быстрее, иначе он заполнится и остановит выполнение. Что делает полный барьер / забор, так это останавливает текущий поток до тех пор, пока буфер хранилища не будет истощен , поэтому наши последующие загрузки появляются в глобальном порядке после наших предыдущих хранилищ.
(Сильно упорядоченная модель памяти asm
volatile
для x86 означает, что на x86 может оказаться ближе к вамmo_acq_rel
, за исключением того, что переупорядочение во время компиляции с неатомарными переменными все еще может происходить. Но большинство не-x86 имеют слабоупорядоченные модели памяти, поэтомуvolatile
иrelaxed
примерно такие же слабый насколькоmo_relaxed
позволяет.)источник
atomic
может привести к тому, что разные потоки будут иметь разные значения для одной и той же переменной в кеше . / Facepalm. В кэше - нет, в регистрах процессора - да (с неатомарными переменными); Процессоры используют согласованный кеш. Я бы хотел, чтобы другие вопросы о SO не содержали объяснений,atomic
которые распространяют заблуждения о том, как работают процессоры. (Потому что это полезно понимать по соображениям производительности, а также помогает объяснить, почему атомарные правила ISO C ++ написаны такими, какие они есть.)Однажды интервьюер, который также считал, что volatile бесполезен, поспорил со мной, что оптимизация не вызовет никаких проблем, и имел в виду разные ядра, имеющие отдельные строки кеша и все такое (действительно не понимал, о чем именно он имел в виду). Но этот фрагмент кода при компиляции с -O3 на g ++ (g ++ -O3 thread.cpp -lpthread) показывает неопределенное поведение. В основном, если значение устанавливается до проверки while, оно работает нормально, а если нет, оно переходит в цикл, не беспокоясь о том, чтобы получить значение (которое было фактически изменено другим потоком). В основном я считаю, что значение checkValue только один раз загружается в регистр и никогда не проверяется снова при самом высоком уровне оптимизации. Если для него установлено значение true перед выборкой, он работает нормально, а если нет, он переходит в цикл. Пожалуйста, поправьте меня, если ошибаюсь.
источник
volatile
? Да, это код UB, но это также UB сvolatile
.Вам нужен изменчивый и, возможно, блокирующий.
volatile сообщает оптимизатору, что значение может изменяться асинхронно, таким образом
будет читать флаг каждый раз в цикле.
Если вы отключите оптимизацию или сделаете каждую переменную изменчивой, программа будет вести себя так же, но медленнее. volatile просто означает: «Я знаю, что вы, возможно, только что прочитали это и знаете, что в нем говорится, но если я скажу, прочтите это, то прочтите это».
Блокировка - это часть программы. Так что, кстати, если вы реализуете семафоры, то, помимо прочего, они должны быть изменчивыми. (Не пытайтесь, это сложно, возможно, понадобится небольшой ассемблер или новый атомарный материал, и это уже было сделано.)
источник
volatile
даже в этом случае бесполезен. Но активное ожидание - иногда полезный метод.volatile
означает " запретить переупорядочивание". Вы надеетесь, что это означает, что магазины станут глобально видимыми (для других потоков) в программном порядке. Вот чтоatomic<T>
сmemory_order_release
илиseq_cst
дает вам. Ноvolatile
только дает вам гарантию отсутствия переупорядочения во время компиляции : каждый доступ будет появляться в asm в программном порядке. Полезно для драйвера устройства. И полезно для взаимодействия с обработчиком прерывания, отладчиком или обработчиком сигналов в текущем ядре / потоке, но не для взаимодействия с другими ядрами.volatile
на практике достаточно для проверкиkeep_running
флага, как вы делаете здесь: настоящие процессоры всегда имеют согласованные кеши, которые не требуют ручной очистки. Но нет причин рекомендоватьvolatile
покончитьatomic<T>
с этимmo_relaxed
; вы получите такой же asm.