Когда использовать volatile с многопоточностью?

131

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

Итак, каково использование / цель volatile в многопоточной программе?

Дэвид Престон
источник
3
В некоторых случаях вам не нужна / не нужна защита мьютексом.
Stefan Mai
4
Иногда нормально иметь состояние гонки, иногда нет. Как вы используете эту переменную?
Дэвид Хеффернан,
3
@David: Пример, когда "нормально" участвовать в гонке, пожалуйста?
Джон Диблинг,
6
@John Поехали. Представьте, что у вас есть рабочий поток, который обрабатывает ряд задач. Рабочий поток увеличивает счетчик каждый раз, когда завершает задачу. Главный поток периодически считывает этот счетчик и информирует пользователя о ходе выполнения. Пока счетчик правильно выровнен, чтобы избежать разрывов, нет необходимости синхронизировать доступ. Хоть гонка и есть, но она мягкая.
Дэвид Хеффернан,
5
@John Оборудование, на котором работает этот код, гарантирует, что выровненные переменные не будут повреждены. Если рабочий обновляет n до n + 1, когда читатель читает, читателю все равно, получат ли он n или n + 1. Никаких важных решений приниматься не будет, так как он используется только для отчетности о проделанной работе.
Дэвид Хеффернан,

Ответы:

168

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

С другой стороны, у volatileнего есть некоторая польза, которая может быть не такой очевидной. Его можно использовать почти так же, как если бы он constпомог компилятору показать вам, где вы могли совершить ошибку при доступе к некоторому общему ресурсу незащищенным способом. Это использование обсуждается Александреску в этой статье. . Однако это в основном использование системы типов C ++ таким образом, который часто рассматривается как выдумка и может вызвать неопределенное поведение.

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

В стандарте C ++ 2003 не говорится, что volatileк переменным применяется какая-либо семантика Acquire или Release. Фактически, Стандарт полностью ничего не говорит о многопоточности. Однако на определенных платформах семантика получения и выпуска применяется кvolatile переменным.

[Обновление для C ++ 11]

C ++ 11 Стандарта в настоящее время делает квитирование многопоточности непосредственно в модели памяти и lanuage и предоставляет библиотечные средства для борьбы с ним в кроссплатформенном пути. Однако семантика до volatileсих пор не изменилась. volatileдо сих пор не является механизмом синхронизации. Бьярн Страуструп говорит об этом в TCPPPL4E:

Не используй volatile кроме как в низкоуровневом коде, который имеет дело непосредственно с оборудованием.

Не предполагайте volatileособого значения в модели памяти. Это не. Это не механизм синхронизации, как в некоторых более поздних языках. Для синхронизации, использования atomic, а , mutexили condition_variable.

[/ Конец обновления]

Все вышесказанное относится к самому языку C ++, как определено стандартом 2003 г. (а теперь и стандартом 2011 г.). Однако некоторые конкретные платформы добавляют дополнительные функции или ограничения к тому, что они volatileделают. Так , например, в MSVC 2010 (по крайней мере) Приобретать и Release Семантика действительно относятся к определенным операциям по volatileпеременным. Из MSDN :

При оптимизации компилятор должен поддерживать порядок между ссылками на изменчивые объекты, а также ссылками на другие глобальные объекты. В частности,

Запись в изменчивый объект (изменяемая запись) имеет семантику Release; ссылка на глобальный или статический объект, которая возникает перед записью в изменчивый объект в последовательности команд, произойдет до этой изменчивой записи в скомпилированном двоичном файле.

Чтение изменчивого объекта (volatile read) имеет семантику Acquire; ссылка на глобальный или статический объект, которая возникает после чтения энергозависимой памяти в последовательности команд, будет происходить после этого энергозависимого чтения в скомпилированном двоичном файле.

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

Джон Диблинг
источник
19
Часть меня хочет проголосовать против этого из-за снисходительного тона ответа и первого комментария. «volatile бесполезен» сродни «ручное выделение памяти бесполезно». Если вы можете написать многопоточную программу без volatileнее, это потому, что вы стояли на плечах людей, которые раньше volatileреализовывали библиотеки потоков.
Бен Джексон
20
@Ben, просто потому, что что-то бросает вызов вашим убеждениям, не делает его снисходительным,
Дэвид Хеффернан,
39
@Ben: нет, прочтите, что на volatileсамом деле делает в C ++. То, что сказал @John, правильно , конец истории. Это не имеет ничего общего с кодом приложения и библиотечным кодом или «обычными» и «богоподобными всеведущими программистами» в этом отношении. volatileне нужен и бесполезен для синхронизации между потоками. Библиотеки потоков не могут быть реализованы в терминах volatile; он в любом случае должен полагаться на детали, специфичные для платформы, и когда вы полагаетесь на них, они вам больше не нужны volatile.
jalf
6
@jalf: «volatile не нужен и бесполезен для синхронизации между потоками» (это то, что вы сказали) - это не то же самое, что «volatile бесполезен для многопоточного программирования» (это то, что Джон сказал в ответе). Вы на 100% правы, но я не согласен с Джоном (частично) - volatile все еще можно использовать для многопоточного программирования (для очень ограниченного набора задач)
4
@GMan: Все, что полезно, полезно только при определенном наборе требований или условий. Volatile полезен для многопоточного программирования при строгом наборе условий (а в некоторых случаях может быть даже лучше (для некоторого определения лучше), чем альтернативы). Вы говорите «игнорировать то, что и ..», но случай, когда volatile полезен для многопоточности, ничего не игнорирует. Ты выдумал то, чего я никогда не утверждал. Да, полезность volatile ограничена, но она существует, но мы все можем согласиться с тем, что она НЕ полезна для синхронизации.
31

(Примечание редактора: C ++ 11 volatileне является подходящим инструментом для этой работы и все еще имеет UB гонки данных. Используйте std::atomic<bool>с std::memory_order_relaxedload / store, чтобы сделать это без UB. В реальных реализациях он будет компилироваться в тот же asm, что и volatile. Я добавил ответ с более подробной информацией, а также устранение заблуждений в комментариях о том, что слабоупорядоченная память может быть проблемой для этого варианта использования: все реальные процессоры имеют согласованную общую память, поэтому volatileбудут работать для этого в реальных реализациях C ++. Но все же не не делай этого.

Некоторое обсуждение в комментариях , кажется, говорить о других потребительных случаях , когда вам будет необходимо что - то более сильное , чем расслабленных Атомикс. Этот ответ уже указывает на то, что volatileвам не нужно упорядочивать.)


Volatile иногда бывает полезным по следующей причине: этот код:

/* global */ bool flag = false;

while (!flag) {}

оптимизирован gcc для:

if (!flag) { while (true) {} }

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

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

zeuxcg
источник
4
Если я помню, C ++ 0x atomic предназначен для правильного выполнения того, что многие люди считают (ошибочно), выполняемым volatile.
Дэвид Хеффернан,
14
volatileне предотвращает изменение порядка доступа к памяти. volatileдоступы не будут переупорядочены относительно друг друга, но они не дают никаких гарантий относительно переупорядочения относительно не volatileобъектов, и поэтому они в основном бесполезны в качестве флагов.
jalf
14
@Ben: Я думаю, у тебя все с ног на голову. Толпа "volatile is бесполезна" полагается на простой факт, что volatile не защищает от переупорядочения , а это означает, что он совершенно бесполезен для синхронизации. Другие подходы могут быть столь же бесполезными (как вы упомянули, оптимизация кода во время компоновки может позволить компилятору заглянуть в код, который, как вы предполагали, компилятор будет рассматривать как черный ящик), но это не устраняет недостатки volatile.
jalf 05
15
@jalf: См. статью Арка Робинсона (ссылка в другом месте на этой странице), 10-й комментарий (автор: Spud). По сути, переупорядочение не меняет логики кода. Опубликованный код использует флаг для отмены задачи (а не для того, чтобы сигнализировать о том, что задача выполнена), поэтому не имеет значения, отменена ли задача до или после кода (например: while (work_left) { do_piece_of_work(); if (cancel) break;}если отмена переупорядочивается в цикле, логика все еще в силе.У меня был фрагмент кода, который работал аналогично: если основной поток хочет завершить работу, он устанавливает флаг для других потоков, но это не так ...
15
... имеет значение, если другие потоки выполняют несколько дополнительных итераций своих рабочих циклов до их завершения, если это происходит достаточно скоро после установки флага. Конечно, это ЕДИНСТВЕННОЕ использование, о котором я могу думать, и его довольно нишевая (и может не работать на платформах, где запись в изменчивую переменную не делает изменение видимым для других потоков, хотя по крайней мере на x86 и x86-64 это работает). Я бы, конечно, никому не советовал делать это без очень веской причины, я просто говорю, что общее заявление вроде «volatile НИКОГДА не полезно в многопоточном коде» не на 100% правильное.
16

В C ++ 11 обычно никогда не используйте volatile для потоковой передачи, только для MMIO

Но TL: DR, он действительно "работает" как атомарный mo_relaxedна оборудовании с согласованными кэшами (то есть со всем); достаточно, чтобы компиляторы не сохраняли вары в регистрах. atomicне нужны барьеры памяти для создания атомарности или видимости между потоками, только для того, чтобы текущий поток ожидал до / после операции, чтобы создать порядок между доступами этого потока к различным переменным. mo_relaxedникогда не нуждается в каких-либо барьерах, просто загружайте, храните или RMW.

Для рулонного своего собственного Атомикса с volatile(и инлайн-ассемблер для барьеров) в старые времена до C ++ 11 std::atomic, volatileбыл только хорошим способом получить некоторые вещи для работы . Но это зависело от множества предположений о том, как работают реализации, и никогда не гарантировалось никакими стандартами.

Например, ядро ​​Linux по-прежнему использует собственный атомарный аппарат с ручным управлением. volatile , но поддерживает только несколько конкретных реализаций C (GNU C, clang и, возможно, ICC). Частично это связано с расширениями GNU C и встроенным синтаксисом и семантикой asm, но также потому, что это зависит от некоторых предположений о том, как работают компиляторы.

Для новых проектов это почти всегда неправильный выбор; вы можете использовать std::atomic(with std::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::atomicmo_relaxed устарел volatileдля многопоточности.

(Стандарт ISO C ++ об этом довольно расплывчатый, просто говорится, что volatileдоступы должны оцениваться строго в соответствии с правилами абстрактной машины C ++, а не оптимизироваться. Учитывая, что реальные реализации используют адресное пространство памяти машины для моделирования адресного пространства C ++, это означает, что volatileоперации чтения и присваивания должны компилироваться для загрузки / сохранения инструкций для доступа к объектному представлению в памяти.)


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

    // global
    bool exit_now = false;

    // in one thread
    while (!exit_now) { do_stuff; }

    // in another thread, or signal handler in this thread
    exit_now = true;

Без volatile или atomic правило as-if и предположение об отсутствии UB-гонки данных позволяет компилятору оптимизировать его в asm, который проверяет флаг только один раз , прежде чем войти (или нет) в бесконечный цикл. Именно это и происходит в реальной жизни с настоящими компиляторами. (И обычно оптимизируют большую часть do_stuffиз-за того, что цикл никогда не завершается, поэтому любой последующий код, который мог бы использовать результат, недоступен, если мы войдем в цикл).

 // Optimizing compilers transform the loop into asm like this
    if (!exit_now) {        // check once before entering loop
        while(1) do_stuff;  // infinite loop
    }

Многопоточная программа застряла в оптимизированном режиме, но нормально работает в -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 ++ даже говорит о «согласованности чтения-чтения», «согласованности чтения-записи» и т. Д. Одно примечание в стандарте даже указывает на связь с оборудованием:

http://eel.is/c++draft/intro.races#19

[Примечание: четыре предшествующих требования согласованности эффективно запрещают компилятор переупорядочивать атомарные операции для одного объекта, даже если обе операции являются ослабленными нагрузками. Это фактически обеспечивает гарантию согласованности кеша, предоставляемую большинством оборудования, доступного для атомарных операций 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 сказано:

Эта архитектура (ARMv7) написана с расчетом на то, что все процессоры, использующие одну и ту же операционную систему или гипервизор, находятся в одном и том же домене внутреннего совместного использования.

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

См. Также это обсуждение 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позволяет.)

Питер Кордес
источник
Комментарии не предназначены для расширенного обсуждения; этот разговор был перемещен в чат .
Сэмюэл Лью
2
Отличное описание. Это именно то, что я искал (предоставляя все факты), вместо общего заявления, в котором просто говорится: «Используйте атомарный вместо volatile для одного глобального общего логического флага».
Берни
2
@bernie: Я написал это после того, как разочаровался в повторяющихся утверждениях, что неиспользование atomicможет привести к тому, что разные потоки будут иметь разные значения для одной и той же переменной в кеше . / Facepalm. В кэше - нет, в регистрах процессора - да (с неатомарными переменными); Процессоры используют согласованный кеш. Я бы хотел, чтобы другие вопросы о SO не содержали объяснений, atomicкоторые распространяют заблуждения о том, как работают процессоры. (Потому что это полезно понимать по соображениям производительности, а также помогает объяснить, почему атомарные правила ISO C ++ написаны такими, какие они есть.)
Питер Кордес
-1
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

bool checkValue = false;

int main()
{
    std::thread writer([&](){
            sleep(2);
            checkValue = true;
            std::cout << "Value of checkValue set to " << checkValue << std::endl;
        });

    std::thread reader([&](){
            while(!checkValue);
        });

    writer.join();
    reader.join();
}

Однажды интервьюер, который также считал, что volatile бесполезен, поспорил со мной, что оптимизация не вызовет никаких проблем, и имел в виду разные ядра, имеющие отдельные строки кеша и все такое (действительно не понимал, о чем именно он имел в виду). Но этот фрагмент кода при компиляции с -O3 на g ++ (g ++ -O3 thread.cpp -lpthread) показывает неопределенное поведение. В основном, если значение устанавливается до проверки while, оно работает нормально, а если нет, оно переходит в цикл, не беспокоясь о том, чтобы получить значение (которое было фактически изменено другим потоком). В основном я считаю, что значение checkValue только один раз загружается в регистр и никогда не проверяется снова при самом высоком уровне оптимизации. Если для него установлено значение true перед выборкой, он работает нормально, а если нет, он переходит в цикл. Пожалуйста, поправьте меня, если ошибаюсь.

Ану Сирил
источник
4
При чем тут volatile? Да, это код UB, но это также UB с volatile.
Дэвид Шварц
-2

Вам нужен изменчивый и, возможно, блокирующий.

volatile сообщает оптимизатору, что значение может изменяться асинхронно, таким образом

volatile bool flag = false;

while (!flag) {
    /*do something*/
}

будет читать флаг каждый раз в цикле.

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

Блокировка - это часть программы. Так что, кстати, если вы реализуете семафоры, то, помимо прочего, они должны быть изменчивыми. (Не пытайтесь, это сложно, возможно, понадобится небольшой ассемблер или новый атомарный материал, и это уже было сделано.)

Ctrl-Alt-Делор
источник
1
Но разве этот и тот же пример в другом ответе не занят ожиданием и, следовательно, чего-то, чего следует избегать? Если это надуманный пример, есть ли какие-нибудь не надуманные примеры из реальной жизни?
Дэвид Престон
7
@Chris: Время от времени ожидание - хорошее решение. В частности, если вы ожидаете, что вам придется подождать всего пару тактов, это несет гораздо меньше накладных расходов, чем гораздо более тяжелый подход к приостановке потока. Конечно, как я упоминал в других комментариях, такие примеры, как этот, ошибочны, потому что предполагают, что операции чтения / записи во флаг не будут переупорядочены относительно кода, который он защищает, и такой гарантии не дается, и поэтому , volatileдаже в этом случае бесполезен. Но активное ожидание - иногда полезный метод.
jalf 05
3
@richard Да и нет. Первая половина правильная. Но это только означает, что ЦП и компилятор не могут переупорядочивать изменчивые переменные относительно друг друга. Если я прочитал изменчивую переменную A, а затем прочитал изменчивую переменную B, то компилятор должен выдать код, который гарантированно (даже с переупорядочением ЦП) прочитает A раньше B. Но он не дает никаких гарантий относительно всех обращений к энергонезависимой переменной , Их можно легко переупорядочить в соответствии с вашим изменчивым чтением / записью. Поэтому, если вы не сделаете каждую переменную в своей программе изменчивой, это не даст вам той гарантии, которая вас интересует,
jalf
2
@ ctrl-alt-delor: Это не то, что volatileозначает " запретить переупорядочивание". Вы надеетесь, что это означает, что магазины станут глобально видимыми (для других потоков) в программном порядке. Вот что atomic<T>с memory_order_releaseили seq_cstдает вам. Но volatile только дает вам гарантию отсутствия переупорядочения во время компиляции : каждый доступ будет появляться в asm в программном порядке. Полезно для драйвера устройства. И полезно для взаимодействия с обработчиком прерывания, отладчиком или обработчиком сигналов в текущем ядре / потоке, но не для взаимодействия с другими ядрами.
Питер Кордес,
1
volatileна практике достаточно для проверки keep_runningфлага, как вы делаете здесь: настоящие процессоры всегда имеют согласованные кеши, которые не требуют ручной очистки. Но нет причин рекомендовать volatileпокончить atomic<T>с этим mo_relaxed; вы получите такой же asm.
Питер Кордес