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

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

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


person David Preston    schedule 29.12.2010    source источник
comment
В некоторых случаях вам не нужна / не нужна защита мьютексом.   -  person Stefan Mai    schedule 30.12.2010
comment
Иногда нормально иметь состояние гонки, иногда нет. Как вы используете эту переменную?   -  person David Heffernan    schedule 30.12.2010
comment
@David: Пример, когда можно устроить гонку, пожалуйста?   -  person John Dibling    schedule 30.12.2010
comment
@John Поехали. Представьте, что у вас есть рабочий поток, который обрабатывает ряд задач. Рабочий поток увеличивает счетчик каждый раз, когда завершает задачу. Главный поток периодически считывает этот счетчик и информирует пользователя о ходе выполнения. Пока счетчик правильно выровнен, чтобы избежать разрывов, нет необходимости синхронизировать доступ. Хотя есть гонка, она доброкачественная.   -  person David Heffernan    schedule 30.12.2010
comment
@David: Было бы сложно оценить безопасность такого устройства без полного изучения кода. Даже если бы проверка пришла к выводу, что записи были атомарными (сомнительно) и полностью записывались через кеш (трудно сказать), я бы все равно отклонил это как плохой код. Его безопасность была бы чрезвычайно хрупкой и легко нарушалась бы малейшими изменениями кода. Программисты по техническому обслуживанию легко сломают это устройство, и проблемы могут не проявиться при тестировании.   -  person John Dibling    schedule 30.12.2010
comment
@John Оборудование, на котором работает этот код, гарантирует, что выровненные переменные не будут повреждены. Если рабочий обновляет n до n + 1, когда читатель читает, читателю все равно, получат он n или n + 1. Никаких важных решений приниматься не будет, так как он используется только для отчетности о проделанной работе.   -  person David Heffernan    schedule 30.12.2010
comment
@ Дэвид: Думаю, я не понимаю, что ты имеешь в виду, говоря о слезах.   -  person John Dibling    schedule 30.12.2010
comment
@John re tearing, я предлагаю вам следующее от Джо Даффи: msdn.microsoft. ru / en-us / magazine / cc817398.aspx   -  person David Heffernan    schedule 30.12.2010
comment
@ Дэвид: Вау, стена текста. :) Но спасибо, еще не читал. Я сделаю это, когда у меня будет возможность.   -  person John Dibling    schedule 30.12.2010
comment
@John Это все отличные вещи, но бит о разрыве - всего пара абзацев.   -  person David Heffernan    schedule 30.12.2010
comment
isvolatileusefulwiththreads.com (также @DavidHeffernan, software.intel.com/en-us/blogs/2013/01/06 / - это материал, который необходимо прочитать о безвредных гонках за данные)   -  person Jonathan Wakely    schedule 18.03.2015
comment
Когда использовать: никогда.   -  person alecov    schedule 14.01.2017
comment
@JohnDibling Пример того, когда можно устраивать гонки Когда можно использовать мьютекс. Или атомный. Практически все нетривиальные программы MT имеют безобидные условия гонки.   -  person curiousguy    schedule 15.07.2018


Ответы (5)


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

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

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

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

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

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

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

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

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

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

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

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

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

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

person John Dibling    schedule 29.12.2010
comment
+1 Очень хорошие ссылки. Семантику volatile не следует путать между языками, включая то, что можно найти в словаре :-) (Он несет очень строгую семантику модели памяти в Java, например, но это другой язык / среда). - person ; 30.12.2010
comment
Некоторые компиляторы предоставляют дополнительную семантику volatile, которая полезна для многопоточной разработки, но это определенно не является частью стандарта. - person Michael; 30.12.2010
comment
Часть меня хочет проголосовать против этого из-за снисходительного тона ответа и первого комментария. volatile бесполезно сродни ручному выделению памяти бесполезно. Если вы можете написать многопоточную программу без volatile, это потому, что вы стояли на плечах людей, которые volatile использовали библиотеки потоковой передачи. - person Ben Jackson; 30.12.2010
comment
@ Бен только потому, что что-то бросает вызов твоим убеждениям, не делает его снисходительным - person David Heffernan; 30.12.2010
comment
@John: Я думаю, что первый комментарий о том, что вы не понимаете, повлиял на мою интерпретацию тона. В остальном я прочел это как решительное заявление против использования volatile, что, вероятно, является правильным для большинства людей, пишущих код уровня приложения. - person Ben Jackson; 30.12.2010
comment
@Ben: нет, прочти, что volatile на самом деле делает в C ++. То, что сказал @John, правильно, конец истории. Это не имеет ничего общего с кодом приложения и кодом библиотеки или обычными и богоподобными всеведущими программистами в этом отношении. volatile не нужен и бесполезен для синхронизации между потоками. Библиотеки потоков не могут быть реализованы в терминах volatile; в любом случае он должен полагаться на детали, специфичные для платформы, и когда вы полагаетесь на них, вам больше не нужно volatile. - person jalf; 30.12.2010
comment
@jalf: volatile не нужен и бесполезен для синхронизации между потоками (как вы и сказали) - это не то же самое, что volatile бесполезен для многопоточного программирования (это то, что Джон сказал в ответе). Вы на 100% правы, но я не согласен с Джоном (частично) - volatile все еще можно использовать для многопоточного программирования (для очень ограниченного набора задач) - person ; 12.02.2011
comment
например, см. десятый ответ (от Spud) в комментариях здесь для законного использования volatile: software.intel.com/en-us/blogs/2007/11/30/ (хотя это несколько специфично для x86, поскольку другие платформы могут требовать, чтобы ядра передавали данные друг другу, что явно не годится) - person ; 12.02.2011
comment
@Dan: Итак, вы в основном сказали, что это полезно! ... предполагая это и игнорируя то и это, и .... Все может быть правдой, если вы сделаете достаточно предположений; и вы, безусловно, отказались от C ++ как языка с ними. - person GManNickG; 21.05.2011
comment
@ Джон: ну, ты никогда не говорил, что это бесполезно для всего, и я никогда не говорил, что ты это говоришь. Вы по-прежнему говорили, что это бесполезно для многопоточного программирования, и я это оспариваю. Это не особенно полезно, но и не совсем бесполезно. - person ; 21.05.2011
comment
@GMan: Все, что полезно, полезно только при определенном наборе требований или условий. Volatile полезен для многопоточного программирования при строгом наборе условий (а в некоторых случаях может быть даже лучше (для некоторого определения лучше), чем альтернативы). Вы говорите, что игнорируете это и ... но случай, когда volatile полезен для многопоточности, ничего не игнорирует. Ты выдумал то, чего я никогда не утверждал. Да, полезность volatile ограничена, но она существует, но мы все можем согласиться с тем, что она НЕ полезна для синхронизации. - person ; 21.05.2011
comment
@Dan: Я хотел сказать, что volatile - это концепция языка C ++, а не концепция реализации. Тем не менее, вы сказали, что это было полезно из-за некоторых деталей реализации. Это не влияет на его полезность в C ++. - person GManNickG; 21.05.2011
comment
@JohnDib: я думаю, вам следует подумать о добавлении к своему ответу того, что volatile имеет выпуск и приобретение [семантика в Visual C ++] (msdn.microsoft.com/en-us/library/12a04hfd%28v=vs.80%29.aspx) (также см. мой ответ здесь: stackoverflow.com/questions/6995310/) - person Martin Ba; 09.08.2011
comment
@Martin: Я недавно внес в этот ответ обширное подробное редактирование. Я включил некоторые детали, специфичные для платформы. - person John Dibling; 12.07.2012
comment
Я с трудом могу вспомнить реальный пример использования volatile в обычном программировании на уровне приложений, есть ли такой? - person baye; 29.08.2012
comment
Что такое Windows 2010? - person fredoverflow; 19.01.2015
comment
@FredOverflow: опечатка. - person John Dibling; 20.01.2015
comment
Необходимо обновить, чтобы упомянуть std::atomic<T>. - person Mgetz; 17.03.2015
comment
@Mgetz: правка от 20 января включила упоминание об атомных и других устройствах. - person John Dibling; 18.03.2015
comment
Не используйте volatile, кроме как в низкоуровневом коде, который имеет дело непосредственно с оборудованием. Категорически не согласен. Volatile полезен для обработчиков сигналов, для некоторых случаев многопоточного кода, для тестирования ... Это просто не замена атомике в большинстве случаев. - person curiousguy; 28.06.2018
comment
Уловка @curiousguy Unix self-pipe более полезна в обработчиках сигналов, чем volatile, и позволяет создавать гораздо более надежный код. - person Maxim Egorushkin; 25.07.2018

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

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


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

/* global */ bool flag = false;

while (!flag) {}

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

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

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

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

person zeuxcg    schedule 29.12.2010
comment
Если я помню, C ++ 0x atomic предназначен для правильного выполнения того, что многие люди считают (ошибочно), выполняемым volatile. - person David Heffernan; 30.12.2010
comment
Ага. Забавно, я добавил строчку об атомарных объектах C ++ 0x, но затем удалил ее, поскольку она, казалось, не связана с конкретным вопросом :) C ++ 0x atomic как и должно быть - конкретная семантика загрузки / сохранения на каждый доступ очень полезен. - person zeuxcg; 30.12.2010
comment
volatile не предотвращает изменение порядка доступа к памяти. volatile доступы не будут переупорядочены относительно друг друга, но они не дают никаких гарантий относительно переупорядочения относительно объектов, отличных от volatile, и поэтому они в основном бесполезны в качестве флагов. - person jalf; 30.12.2010
comment
Я думаю, что большая часть изменчивой и бесполезной толпы полагается на тот факт, что более вероятный код, такой как while (!global_flag) { sleep(1); }, будет работать без volatile, потому что их компиляторы не смотрят sleep() при оптимизации и, таким образом, предполагают, что sleep() может изменять global_flag, и, следовательно, правильный код произведены по неправильным причинам. Может быть, подходящий пример можно было бы построить с оптимизацией времени компоновки LLVM? - person Ben Jackson; 30.12.2010
comment
Но этот код использует активное ожидание - чего следует избегать. - person David Preston; 03.01.2011
comment
@Ben: Я думаю, у тебя все с ног на голову. Группа volatile - это бесполезная толпа полагается на тот простой факт, что volatile не защищает от переупорядочения, а это означает, что он совершенно бесполезен для синхронизации. Другие подходы могут быть столь же бесполезными (как вы упомянули, оптимизация кода во время компоновки может позволить компилятору заглянуть в код, который, как вы предполагали, компилятор будет рассматривать как черный ящик), но это не устраняет недостатки volatile. - person jalf; 05.01.2011
comment
@jalf: некоторые виды использования флагов не требуют, чтобы они не переупорядочивались (хотя в большинстве случаев это так, поэтому, хотя существует очень нишевый вариант использования, когда использование volatile вполне нормально, есть много и много людей, которые злоупотребляют volatile небезопасными способами ) - person ; 12.02.2011
comment
@Dan: назовите ситуацию, когда переупорядочивание не проблема. Флаг используется, чтобы указать, что произошло какое-то событие, и это работает, только если событие действительно произошло при установке флага. Какие еще случаи вы имеете в виду? - person jalf; 13.02.2011
comment
@jalf: См. статью Arch Robinson (ссылка в другом месте на этой странице), 10-й комментарий (Spud). По сути, переупорядочение не меняет логики кода. Опубликованный код использует флаг для отмены задачи (а не для того, чтобы сигнализировать о том, что задача выполнена), поэтому не имеет значения, отменена ли задача до или после кода (например: while (work_left) { do_piece_of_work(); if (cancel) break;}, если отмена переупорядочена в цикле , логика все еще в силе.У меня был фрагмент кода, который работал аналогично: если основной поток хочет завершить работу, он устанавливает флаг для других потоков, но это не ... - person ; 13.02.2011
comment
... имеет значение, если другие потоки выполняют несколько дополнительных итераций своих рабочих циклов перед завершением, если это происходит достаточно скоро после установки флага. Конечно, это ЕДИНСТВЕННОЕ использование, о котором я могу думать, и его довольно узкая ниша (и может не работать на платформах, где запись в изменчивую переменную не делает изменение видимым для других потоков, хотя, по крайней мере, на x86 и x86-64 это работает). Я бы, конечно, никому не советовал делать это без очень веской причины, я просто говорю, что общий оператор, такой как volatile, НИКОГДА не полезен в многопоточном коде, неверен на 100%. - person ; 13.02.2011
comment
Поскольку я искал хороший ответ, я мог бы сослаться на то, почему volatile бесполезен, я не могу оставить эти комментарии здесь в силе. То, что здесь говорит Дэн, работает только для x86 из-за сильной базовой модели памяти. То, что он предлагает здесь, так же не работает, как и любой другой вариант использования volatile на многих других платформах (например, нет гарантии, что вы не прочитаете устаревшее значение из кеша). Так что да, если вам нужна программа, которая работает не только под x86, volatile действительно никогда не пригодится. - person Voo; 02.03.2014
comment
Я подытожу все это: (1.) существуют примеры, когда volatile является правильным / полезным для многопоточности (пример @ Dan: используйте volatile bool для остановки цикла потока. другой пример) (1. приложение) примеры, в которых volatile является правильным / полезным для многопоточности, зависят от компилятора / реализации (один компилятор может иметь атомарное поведение для volatile, а другой - нет. ) И зависит от оборудования (arm vs x86) (3.) volatile-выражение НИКОГДА не является правильным / полезным в многопоточном коде неверно, потому что существует хотя бы один пример счетчика - person Trevor Boyd Smith; 06.05.2015
comment
@Voo: существует много платформ, на которых можно дешево гарантировать, что запись в изменчивую переменную с однократной записью в конечном итоге будет замечена во всех других потоках (в течение нескольких секунд ); действительно, оптимизации можно было бы помочь, если бы существовал квалификатор, который был бы слабее, чем volatile, так что компилятор был бы свободен оптимизировать ограниченное количество последовательных обращений [например, если компилятор развернет цикл 8 раз, ему нужно будет проверить переменную только один раз в развернутом цикле, а не восемь раз]. Накладные расходы, связанные с механизмом со слабой семантикой ... - person supercat; 16.07.2015
comment
... может быть намного меньше, чем было бы необходимо при использовании механизма с более жесткой семантикой. - person supercat; 16.07.2015
comment
@supercat, что как раз то, что std :: atomic делает с моделями памяти более слабыми, чем получение / выпуск. - person Voo; 16.07.2015
comment
@Trevor Так вы также думаете, что ссылка на один за концом массива не всегда неверна, потому что есть один пример, в котором это работает? Это не то, как работает c, и это особенно опасно думать, если задействован параллелизм. - person Voo; 16.07.2015
comment
@Voo, если под ссылкой на один за концом массива вы имеете в виду точку за одним концом массива, то вы небрежны с терминологией, поскольку этот не 't ссылается на память, на которую он указывает. - person Johann Gerell; 09.10.2015
comment
@Johann Ты просто придираешься или действительно запутались? Я бы подумал, что любой программист на C ++ знает, что доступ к памяти за пределами массива - это неопределенное поведение, но все же работает достаточно часто - в этом весь смысл аргументации. Но да, я потерял там память, и нет, я не имел в виду указывать один за концом массива, потому что это не неопределенное поведение. - person Voo; 09.10.2015
comment
@Voo Нет, я искренне не знал, имели ли вы в виду указывать на, когда говорили ссылка - я не считаю придиркой указывать (sic!), Когда эти двое термины используются неправильно, поскольку они совершенно разные. Теперь, когда вы пояснили, что имели в виду, что доступ к памяти путем ссылки на один за концом массива может по-видимому быть успешным, я прекрасно понимаю ваше утверждение. - person Johann Gerell; 09.10.2015
comment
@Voo Просто чтобы прояснить, почему я вообще ответил; вы написали Я бы подумал, что любой программист на C ++ знает, что доступ к памяти за пределами массива является неопределенным поведением, но я бы сказал, что подавляющее большинство разработчиков C ++ не знают, что UB означает и не знает разницу между point at и reference. Это большинство похоже на черное дело, и вряд ли его можно будет найти на SO, конференциях или чтении блогов C ++ - их все еще большинство. - person Johann Gerell; 09.10.2015
comment
@Voo, повторно ваш предыдущий комментарий о чтении устаревшего значения из кеша: все использует согласованные кеши данных (MESI) software.rajivprab.com/2018/04/29/; asm store в одном ядре будет быстро замечен загрузкой asm в других ядрах. volatile на практике работает для keep_running флага, который не требует упорядочивания. что-нибудь еще, но atomic<bool> с mo_relaxed даст вам тот же asm, что и volatile, и не имеет UB. - person Peter Cordes; 24.10.2019
comment
@Voo: опубликовал мой собственный ответ и обновил это с отказом от ответственности. Этот поток комментариев кажется беспорядочным, и некоторые люди возражают против volatile, потому что он не упорядочивает, даже если для варианта использования в этом ответе он не нужен. (Предполагая, что это флаг keep_running, а не флаг data_ready.) - person Peter Cordes; 24.10.2019
comment
@Peter Все использует согласованные кеши данных, все? Я имею в виду, что ваш обычный процессор x64 будет работать, но каждый ISA, когда-либо изобретенный сейчас и в будущем? Есть несколько очень странных архитектур. Я не могу себе представить, что все эти DSP или смешанные SOC ЦП гарантируют что-либо близкое к параллелизму MESI. - person Voo; 24.10.2019
comment
@Voo: Я имею в виду не только x86-64. Я имею в виду PowerPC, MIPS, ARM, RISC-V и т.д. и т.д. Смешанные SOC с некогерентной общей памятью, по-видимому, существуют, но не AFAIK в качестве целей компилятора для компиляторов, совместимых с ISO C ++. (Обсуждение в комментариях о них, на которые я ссылался в своем ответе на этот вопрос.) - person Peter Cordes; 24.10.2019
comment
@Peter Но это моя точка зрения: теперь мы уже ослабили и без того слабое требование об отсутствии компилятора, о существовании которого я знаю в настоящее время для этих архитектур, о существовании которых я не знал 4 часа назад. Но зачем преодолевать все эти неприятности, если есть простое решение, которое действительно гарантированно работает идеально? (Конечно, я вполне уверен, что все эти DSP нарушают некоторые правила стандарта, но будут ли они и все будущие?) - person Voo; 24.10.2019
comment
Но зачем испытывать все эти проблемы, когда есть простое решение, которое действительно гарантированно работает отлично? FFS, почему люди продолжают думать, что я выступаю за использование _1 _ ??? Это действительно расстраивает. Я пытаюсь способствовать пониманию кешей в основных процессорах, чтобы люди могли принимать правильные решения о производительности при правильном использовании atomic<T> (возможно, с mo_relaxed). Вы читали мою правку к этому ответу? Или любой текст, выделенный жирным шрифтом или ## заголовок в моем ответе? Все они тщательно сформулированы, чтобы сказать не на самом деле использовать volatile, но вот как работают процессоры. - person Peter Cordes; 24.10.2019
comment
@Voo Вы упускаете суть. Дело не в том, что странных ЦП не существует или необычных способов заставить обычный ЦП работать на одной материнской плате. Дело в том, что для них невозможно представить себе разумную стратегию реализации потока. Любой компилятор C или C ++ пытается обеспечить предсказуемую производительность, по крайней мере, для общих шаблонов кода. То, что вы можете эмулировать машину Тьюринга на каком-то процессоре, не означает, что у вас может быть разумный компилятор. Вы, как ppl, утверждаете, что vtables не являются стандартными. Все компиляторы их используют. - person curiousguy; 10.11.2019
comment
@curiousguy Дело в том, что на них невозможно представить себе разумную стратегию реализации потоков. Вы знаете, что эти странные процессоры имеют реализации потоковой передачи (только не стандартные), верно? Отсутствие воображения - не то же самое, что невозможно. - person Voo; 10.11.2019
comment
@Voo Конечно, странный процессор может иметь очень странные потоки. Но это не имеет значения с точки зрения переносимого кода , поскольку эти специальные компиляторы не пытаются поддерживать нормальный код, нуждаются в определенных барьерах и определенных устройствах синхронизации и т. Д. Любая система, в которой вам нужно очищать кеши при больших затратах, чтобы получить доступ к памяти, переносимый код MT не запускается. Код будет написан специально с учетом этих ограничений. - person curiousguy; 10.11.2019
comment
Только представьте себе shared_ptr в такой системе без согласованных кешей. При любом уничтожении экземпляра, когда count>1 вам придется записать измененное содержимое кеша в основную память, а когда count==1 вам даже нужно будет записать обратно в кеш, а затем очистить его, чтобы получить свежие данные? Могли бы вы вообще иметь динамическое размещение в общем домене в этой системе? - person curiousguy; 10.11.2019

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

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

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

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

Для новых проектов это почти всегда неправильный выбор; вы можете использовать std::atomicstd::memory_order_relaxed), чтобы компилятор генерировал такой же эффективный машинный код, как и volatile. std::atomic с mo_relaxed устаревшим volatile для потоковой передачи. (кроме, возможно, исправлять ошибки пропущенной оптимизации с atomic<double> в некоторых компиляторах.)

Внутренняя реализация std::atomic в основных компиляторах (таких как gcc и clang) не просто использует volatile внутри; компиляторы напрямую предоставляют встроенные функции атомарной загрузки, хранения и RMW. (например, встроенные __atomic GNU C, которые работают с простыми объектами.)


Volatile можно использовать на практике (но не делайте этого)

Тем не менее, volatile можно использовать на практике для таких вещей, как флаг exit_now на всех (?) Существующих реализациях C ++ на реальных процессорах, из-за того, как работают процессоры (согласованные кеши) и общих предположений о том, как volatile должен работать. Но не более того, и это не рекомендуется. Цель этого ответа - объяснить, как на самом деле работают существующие процессоры и реализации C ++. Если вас это не волнует, все, что вам нужно знать, это то, что std::atomic volatile устаревшие 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;

Без изменчивости или атомарности, правило «как если бы» и предположение об отсутствии гонки данных 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 вы просто получите полный барьер после сохранения).

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> также превращают += и так далее в атомарные операции 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 volatiles, эквивалент C ++ atomic<T> с порядком памяти seq_cst.)

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

По теме: мой ответ на Безопасен ли 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/). (А загрузка-загрузка означает упорядочение доступа к кеш-памяти в другом ядре).

Команда барьера памяти просто блокирует загрузку и / или сохранение текущего потока до тех пор, пока буфер хранения не иссякнет; это всегда происходит как можно быстрее само по себе. (Или для барьеры LoadLoad / LoadStore, блокировка до завершения предыдущих загрузок.) (Гарантирует ли барьер памяти, что согласованность кеша завершена? устраняет это заблуждение). Так что, если вам не нужен заказ, просто укажите видимость в других потоках, mo_relaxed в порядке. (И volatile, но не делайте этого.)

См. Также сопоставления C / C ++ 11 с процессорами

Интересный факт: на x86 каждое хранилище asm является хранилищем выпуска, потому что модель памяти x86 в основном представляет собой seq-cst плюс буфер хранилища (с пересылкой хранилища).


Наполовину связанный буфер re: store, глобальная видимость и согласованность: C ++ 11 гарантирует очень мало. Большинство реальных ISA (кроме PowerPC) действительно гарантируют, что все потоки могут согласовать порядок появления двух хранилищ двумя другими потоками. (В формальной терминологии модели памяти компьютерной архитектуры они являются атомарными с множеством копий).

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

(Сильно упорядоченная модель памяти asm x86 означает, что volatile на x86 может в конечном итоге дать вам значение, близкое к mo_acq_rel, за исключением того, что переупорядочение во время компиляции с неатомарными переменными все еще может происходить. Но большинство не-x86 имеют слабоупорядоченные модели памяти, поэтому volatile и relaxed настолько слабы, насколько позволяет mo_relaxed.)

person Peter Cordes    schedule 24.10.2019
comment
Комментарии не подлежат расширенному обсуждению; этот разговор был перешел в чат. - person Samuel Liew♦; 08.11.2019
comment
Отличное описание. Это именно то, что я искал (давая все факты) вместо общего оператора, который просто говорит, что использовать атомарный вместо volatile для одного глобального общего логического флага. - person bernie; 28.11.2019
comment
@bernie: Я написал это после того, как разочаровался в повторяющихся утверждениях, что неиспользование atomic может привести к тому, что разные потоки будут иметь разные значения для одной и той же переменной в кеше. / facepalm. В кеше - нет, в CPU регистрах - да (с неатомарными переменными); Процессоры используют согласованный кеш. Я бы хотел, чтобы другие вопросы о SO не были полны объяснений atomic, которые распространяют неправильные представления о том, как работают процессоры. (Потому что это полезно понимать по соображениям производительности, а также помогает объяснить, почему атомарные правила ISO C ++ написаны такими, какие они есть.) - person Peter Cordes; 28.11.2019
comment
@PeterCordes С порядком seq_cst по умолчанию, который вы получаете из while (! Flag), он также добавляет гарантии порядка wrt. неатомарный доступ вы говорите, что mo_seq_cst запрещает переупорядочивание non-mo_seq_cst вокруг mo_seq_cst? - person Daniel Nitzan; 30.01.2021
comment
@DanielNitzan: да, загрузка seq_cst может синхронизироваться с выпуском или хранилищем seq-cst в другом потоке, поэтому любые загрузки в источнике после этого ожидания вращения должны быть также после него в asm. Потому что ISO C ++ говорит, что безопасно читать неатомарные переменные, которые были записаны до этого хранилища релизов (при условии, что они еще не записываются другими более поздними хранилищами). Однако это не двусторонний барьер; теоретически загрузка seq_cst может произойти раньше, чем она отображается в исходном порядке. На практике IDK, если gcc / clang объединит более ранние обращения с более поздними при загрузке seq_cst. (приблизительные описания ...) - person Peter Cordes; 30.01.2021
comment
@PeterCordes, да, операции seq_cst имеют семантику strel / ldacq. - person Daniel Nitzan; 30.01.2021
comment
@PeterCordes И загрузка-получение означает упорядочивание доступа к кешу в другом ядре; здесь вы заявили, что часть LoadLoad семантики чтения-получения гарантируется очисткой машины порядка памяти. Это дешевле, чем слепое упорядочивание загрузочных доступов к кешу. - person Daniel Nitzan; 30.01.2021
comment
@PeterCordes Команда барьера памяти просто блокирует загрузку и / или сохранение текущего потока до тех пор, пока буфер хранилища не иссякнет; должен ли быть барьер полной памяти? или, по крайней мере, конкретно барьер StoreLoad? - person Daniel Nitzan; 30.01.2021
comment
Что делает полный барьер / забор, так это останавливает текущий поток до тех пор, пока буфер хранилища не будет истощен Не полный останов, выполнение OoO все еще может продолжаться, то есть арифметические инструкции для регистров, спекулятивное выполнение хранилищ и загрузок и т. д. - person Daniel Nitzan; 30.01.2021
comment
@DanielNitzan: да, иногда я упрощаю, чтобы передать основную концепцию, замалчивая подобные детали. Обратите внимание, что MFENCE на Skylake, к сожалению, действительно останавливает весь поток, так как подробное описание реализации пост-обновления микрокода. Очевидно, Intel хотела, чтобы MFENCE заказывала загрузку NT из памяти WC, но раньше этого не делала, и единственный способ сделать это с помощью простого обновления микрокода - просто добавить к ней LFENCE. Но, к счастью, они не замедлили locked инструкции, так что они являются предпочтительным препятствием. - person Peter Cordes; 30.01.2021
comment
@DanielNitzan: блокирует загрузку и / или сохранение текущего потока до тех пор, пока буфер хранилища не иссякнет - вы правы, это не относится к барьерам LoadLoad или LoadStore. Для StoreStore он блокирует фиксацию более молодых хранилищ до тех пор, пока не будут зафиксированы более старые хранилища, то есть порядок, в котором опустошается буфер хранилища. По сути, это похоже на установку разделителя на конвейерную ленту продуктового магазина, и это действительно означает слив SB до того, как любые более молодые магазины (могут стать видимыми). Во всяком случае, отредактировано, чтобы исправить это, спасибо; это было достаточно конкретным, чтобы на самом деле стать проблемой. - person Peter Cordes; 30.01.2021

#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 перед выборкой, он работает нормально, а если нет, он переходит в цикл. Пожалуйста, поправьте меня, если ошибаюсь.

person Anu Siril    schedule 11.07.2018
comment
При чем здесь volatile? Да, это код UB, но это также UB с volatile. - person David Schwartz; 11.07.2018

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

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

volatile bool flag = false;

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

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

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

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

person ctrl-alt-delor    schedule 31.12.2010
comment
Но разве этот и тот же пример в другом ответе не занят ожиданием и, следовательно, чего-то, чего следует избегать? Если это надуманный пример, есть ли какие-нибудь не надуманные примеры из реальной жизни? - person David Preston; 03.01.2011
comment
Да, хорошо. Вам действительно нужно что-то делать в фигурных скобках (пост редактирую). Ожидание - обычно плохая идея. Возможно, вы что-то обрабатываете (список), пока другой поток не подаст вам сигнал об остановке. Без valatile это будет продолжаться вечно, и в этом примере блокировка не требуется, bool является атомарным. - person ctrl-alt-delor; 05.01.2011
comment
@Chris: Время от времени ожидание - хорошее решение. В частности, если вы ожидаете, что вам придется подождать всего пару тактов, это несет гораздо меньше накладных расходов, чем гораздо более тяжелый подход к приостановке потока. Конечно, как я упоминал в других комментариях, такие примеры, как этот, ошибочны, потому что предполагают, что операции чтения / записи во флаг не будут переупорядочены относительно кода, который он защищает, и такой гарантии не дается, и поэтому , volatile бесполезен даже в этом случае. Но активное ожидание - иногда полезный метод. - person jalf; 05.01.2011
comment
@jalf, как я понимаю, volatile сообщает компилятору, что переменная может быть прочитана / записана асинхронно с программой (другим потоком или моим оборудованием), она должна давать семантику «если я говорю« читать »или« писать, то прочтите или напиши и сделай это, когда я тебе скажу ». Если ЦП переупорядочивает инструкции, которые изменяют эту семантику, компилятор также должен отменить эту оптимизацию. Я что-то упускаю. - person ctrl-alt-delor; 10.01.2015
comment
@richard Да и нет. Первая половина правильная. Но это только означает, что ЦП и компилятор не могут переупорядочивать изменчивые переменные относительно друг друга. Если я прочитал изменчивую переменную A, а затем прочитал изменчивую переменную B, то компилятор должен выдать код, который гарантированно (даже с переупорядочением ЦП) прочитает A раньше B. Но он не дает никаких гарантий относительно всех обращений к энергонезависимой переменной. . Их можно легко переупорядочить в соответствии с вашим изменчивым чтением / записью. Поэтому, если вы не сделаете каждую переменную в своей программе изменчивой, это не даст вам гарантии, в которой вы заинтересованы. - person jalf; 10.01.2015
comment
@jalf Это неправда. Не требуется volatile предотвращать переупорядочивание ЦП, и на большинстве современных платформ это фактически не делается. - person David Schwartz; 27.06.2016
comment
@DavidSchwartz Вы просто публикуете ложные сведения о volatile? У вас другой Стандарт, чем у этих людей? stackoverflow.com/questions/2535148/ - person underscore_d; 07.07.2016
comment
@underscore_d Прочтите комментарии к двум ответам, которые, кажется, не согласны со мной, и вы увидите, что это не так. Вы не найдете доступ к volatiles, излучающим барьеры памяти, поэтому переупорядочение не предотвращено. - person David Schwartz; 07.07.2016
comment
@DavidSchwartz Мы имеем в виду переупорядочивание операций чтения / записи для этого конкретного volatile, которое гарантированно не произойдет, а не переупорядочение любого указанного чтения / записи относительно других операторов (независимо от того, являются ли они также volatile), что разрешено. Последнее, что вы имеете в виду? - person underscore_d; 07.07.2016
comment
@underscore_d Опять же, вы увидите, что практически на каждом компиляторе операции чтения и записи в volatile переменные не создают барьеры памяти, даже на платформах, которые требуют, чтобы они предотвращали переупорядочивание операций чтения и записи и / или объединились. Попытайся. (Согласны ли вы с тем, что ЦП может объединять записи в обычную память? Вы видите, что компилятор делает что-нибудь, чтобы остановить его, когда две записи происходят в один и тот же volatile? А как насчет записи в два соседних volatile short или записи ABA в них? видите что-нибудь, излучаемое, чтобы остановить объединение записи?) - person David Schwartz; 07.07.2016
comment
@DavidSchwartz Согласны ли вы, что ЦП может объединять записи в обычную память? Какая разница? - person curiousguy; 15.07.2018
comment
@curiousguy Ну, потому что, если он может объединить две записи, он не может сохранить их порядок. Дело в том, что нет абсолютно никаких ограничений на способность ЦП переупорядочивать, изменять или объединять записи. Ничто не накладывает таких ограничений, и фактические процессоры это делают. - person David Schwartz; 16.07.2018
comment
Не должен ли компилятор C выдавать граничные инструкции между чтением и записью изменчивой информации, чтобы гарантировать соблюдение изменчивости? Или существуют процессоры, которые не позволяют реализовать изменчивую информацию? - person ctrl-alt-delor; 16.07.2018
comment
@DavidSchwartz Каково определение слияния двух записей? - person curiousguy; 16.07.2018
comment
@curiousguy Код говорит, что нужно сделать две или три записи, а ЦП вместо этого делает одну или две. Например, вы выполняете i=1; j=2; k=3;, а ЦП выполняет i=1; k=3; за одну атомарную операцию. Ничто не мешает ЦП делать это, даже если i, j и k равны volatile и это делают настоящие ЦП. ЦП могут даже оптимизировать запись в volatile, а реальные ЦП действительно это делают. Таким образом, i=1; j=2; i=3; может привести к i=1; оптимизации записи снова, даже если i равно volatile. Вот почему многие процессоры имеют операции с барьером памяти, и volatile они не используются и не обязательны. - person David Schwartz; 17.07.2018
comment
Действительно, со всеми переменными, начинающимися с 0, если i=1; j=2; i=3; превратить в j=2; i=3;, это нарушит другой поток, ожидающий (ошибочно), что j==2 может произойти, только если i>0. - person curiousguy; 18.07.2018
comment
@ ctrl-alt-delor: Не это означает, что volatile не переупорядочивает. Вы надеетесь, что это означает, что хранилища станут глобально видимыми (для других потоков) в программном порядке. Вот что дает вам atomic<T> с memory_order_release или seq_cst. Но volatile только дает вам гарантию отсутствия переупорядочения во время компиляции: каждый доступ будет появляться в asm в программном порядке. Полезно для драйвера устройства. И полезно для взаимодействия с обработчиком прерывания, отладчиком или обработчиком сигнала в текущем ядре / потоке, но не для взаимодействия с другими ядрами. - person Peter Cordes; 24.10.2019
comment
volatile на практике достаточно для проверки keep_running флага, как вы это делаете здесь: настоящие процессоры всегда имеют согласованные кеши, которые не требуют ручной очистки. Но нет причин рекомендовать volatile вместо atomic<T> с mo_relaxed; вы получите такой же asm. - person Peter Cordes; 24.10.2019