Почему volatile не считается полезным в многопоточном программировании на C или C ++?

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

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

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

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


person Michael Ekstrand    schedule 20.03.2010    source источник
comment
Все, что делает volatile, это говорит о том, что компилятор не должен кэшировать доступ к изменчивой переменной. Он ничего не говорит о сериализации такого доступа. Это обсуждалось здесь, я не знаю, сколько раз, и я не думаю, что этот вопрос что-то добавит к этим обсуждениям.   -  person    schedule 21.03.2010
comment
И снова вопрос, который этого не заслуживает, и который задавали здесь много раз, прежде чем получил голосование. Не могли бы вы прекратить это делать.   -  person    schedule 21.03.2010
comment
@neil Я искал другие вопросы и нашел один, но любое существующее объяснение, которое я видел, почему-то не вызвало того, что мне нужно, чтобы действительно понять, почему я был неправ. Этот вопрос вызвал такой ответ.   -  person Michael Ekstrand    schedule 21.03.2010
comment
Для более глубокого изучения того, что процессоры делают с данными (через свои кеши), посетите: rdrop.com/users/paulmck/scalability/paper/whymb.2010.06.07c.pdf   -  person Sassafras_wot    schedule 04.05.2012
comment
В Java volatile создает барьер памяти при чтении, поэтому его можно использовать в качестве поточно-ориентированного флага завершения метода, поскольку он обеспечивает связь «происходит раньше» с кодом до установки флага. В C.   -  person Monstieur    schedule 14.04.2015
comment
@Monstieur Java volatile не имеет ничего общего с C / C ++ volatile: Java volatile определяется внутри языковой модели, может быть преобразован, оптимизирован (запись с последующим чтением Java volatile может быть оптимизирован для записи) и volatile полностью исключен, если изменчивый переменная не используется совместно между потоками. В C / C ++ volatile находится за пределами языковой модели, операции с изменчивыми объектами могут иметь эффекты, видимые для других устройств, и по определению невозможно преобразование volatile, даже чтение, результат которого игнорируется, не может быть исключен. У них разные цели.   -  person curiousguy    schedule 15.11.2018
comment
@curiousguy Это то, что я имел в виду, имея в виду не C, где его можно использовать для записи в аппаратные регистры и т. д., и он не используется для многопоточности, как это обычно используется в Java.   -  person Monstieur    schedule 15.11.2018


Ответы (9)


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

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

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

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

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

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

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

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

person jalf    schedule 20.03.2010
comment
Спасибо. Насколько я могу судить, в моем понимании мне не хватало того, что volatile не гарантирует отсутствие переупорядочения с соседними доступами к энергонезависимой памяти, что требуется для случая использования атомарной установки флага. - person Michael Ekstrand; 21.03.2010
comment
Действительно ли он гарантирует порядок и запрещает использование кешированных значений или делает это только на уровне компилятора? Если бы первое было правдой, вы могли бы написать переносимую синхронизацию потоков, но весь код, который я видел, использует специфичные для ЦП инструкции и предполагает, что ЦП все переупорядочит. - person ; 21.03.2010
comment
@jbcreix: О чем вы спрашиваете? Барьеры энергозависимости или памяти? В любом случае ответ почти такой же. Они оба должны работать как на уровне компилятора, так и на уровне ЦП, поскольку они описывают наблюдаемое поведение программы - поэтому они должны гарантировать, что ЦП не переупорядочивает все, изменяя поведение, которое они гарантируют. Но в настоящее время вы не можете написать переносимую синхронизацию потоков, потому что барьеры памяти не являются частью стандартного C ++ (поэтому они не переносимы), а volatile недостаточно сильны, чтобы быть полезными. - person jalf; 21.03.2010
comment
Пример MSDN делает это и утверждает, что инструкции не могут быть переупорядочены после временного доступа: msdn.microsoft.com/en-us/library/12a04hfd (v = vs.80) .aspx - person OJW; 16.09.2011
comment
@OJW: Но компилятор Microsoft переопределяет volatile как полный барьер памяти (предотвращающий переупорядочение). Это не является частью стандарта, поэтому вы не можете полагаться на такое поведение в переносимом коде. - person jalf; 04.10.2011
comment
Итак, как же помогают барьеры памяти при одновременном доступе с нескольких процессоров? - person Skizz; 05.07.2012
comment
@Skizz Короче говоря, волшебство процессора и компилятора (и да, вам нужно и то, и другое, чтобы это работало). Барьер памяти - это инструкция ЦП, которая предназначена для решения этой проблемы. Он взаимодействует с шиной памяти, гарантируя, что никакое другое ядро ​​не сможет читать / записывать память до тех пор, пока барьер не завершит свою работу, и ЦП не знает, чтобы не переупорядочивать доступ к памяти через него. Его невозможно эмулировать в программном обеспечении. - person jalf; 07.07.2012
comment
@jalf: Но вам все равно понадобится ключевое слово volatile, чтобы гарантировать, что значение, видимое кодом, является значением в памяти. Без него не мог бы оптимизатор кэшировать значение в регистре? Тогда проблема сводится к одновременному доступу. - person Skizz; 09.07.2012
comment
@Skizz: нет, вот тут-то и появляется волшебная часть уравнения компилятора. Барьер памяти должен осознаваться как процессором, так и компилятором. Если компилятор понимает семантику барьера памяти, он знает, как избегать подобных уловок (а также переупорядочивания чтения / записи через барьер). К счастью, компилятор действительно понимает семантику барьера памяти, так что, в конце концов, все работает. :) - person jalf; 10.07.2012
comment
@jalf: Так как же указать, когда требуется барьер памяти? Компилятор не может узнать, будет ли код выполняться на одном ядре / потоке или на нескольких ядрах / потоках. Оптимизатор будет регистрировать значения везде, где это возможно, что приведет к сбою, если два ядра будут обращаться к данным одновременно, поскольку каждый поток / ядро ​​имеет собственную версию значения, хранящуюся в регистрах. Без «volatile» компилятор наверняка ошибется. - person Skizz; 10.07.2012
comment
@Skizz: Я не уверен, что понимаю. Вы указываете барьер памяти, используя ту внутреннюю часть, которую компилятор предоставляет для этой цели (или используя эквивалент C ++ 11). Это то, что вам нужно вставить вручную; вы не можете полагаться на компилятор, создающий барьеры памяти там, где это необходимо. - person jalf; 10.07.2012
comment
@jalf: Ясно, в вашем ответе не было ясно, что барьеры памяти являются расширением языка / функцией C ++ 11. Так что, если у компилятора нет этого расширения, вы застряли с volatile. Я думал, что в C / C ++ (до 11) есть что-то, о чем я не знал. - person Skizz; 10.07.2012
comment
@Skizz: Понятно. Тогда извините за путаницу. Однако, если у компилятора нет такого расширения, тогда вам не повезло в любом случае, потому что volatile сам по себе недостаточен. - person jalf; 10.07.2012
comment
@Skizz: Сами потоки всегда являются платформо-зависимым расширением до C ++ 11 и C11. Насколько мне известно, каждая среда C и C ++, которая предоставляет расширение потоковой передачи, также предоставляет расширение барьера памяти. Тем не менее, volatile всегда бесполезен для многопоточного программирования. (За исключением Visual Studio, где volatile - расширение барьера памяти.) - person Nemo; 09.10.2012
comment
Как вы сказали, volatile полезен для доступа к регистрам ввода-вывода. Стоимость намного выше, если мы используем барьер памяти для реализации такой функции (блокировка потенциального компилятора / аппаратная оптимизация). - person Thomson; 12.11.2013
comment
@Thomson: ты можешь подробнее рассказать об этом? Почему стоимость будет выше, чем с volatile? volatile тоже предотвращает оптимизацию. - person jalf; 12.11.2013
comment
Если мы воспользуемся барьером памяти для имитации / реализации функции volatile, вызовет ли это множество других несвязанных операций чтения / записи? Он также блокирует компилятор для изменения порядка доступа ко всем несвязанным переменным. - person Thomson; 12.11.2013
comment
@Thomson Я не понимаю, почему это может вызвать множество других несвязанных операций чтения / записи. Пункт о переупорядочивании может быть решен в обоих направлениях, в зависимости от использования. Память предотвращает переупорядочивание всех операций чтения / записи, но только в определенный момент времени (когда выполняется барьерный код), тогда как изменчивая переменная только предотвращает переупорядочение по отношению к другим изменчивым чтениям / пишет, но, с другой стороны, предотвращает это для всех обращений к переменной, а не только в определенной точке кода. - person jalf; 12.11.2013
comment
Насколько я понимаю, volatile заставит читать значение всегда из памяти, а не из кеша. Предположим, у нас есть общая переменная (среди потоков), один поток изменяет ее, и переменная не является изменчивой. Предполагая, что мы используем блокировки мьютексов всякий раз, когда получаем доступ к общей переменной. Поскольку общая переменная энергонезависима, как гарантировать, что переменная всегда будет считываться из памяти, а не из кеша? - person Sumit Trehan; 22.07.2015
comment
@SumitTrehan, который неявно присутствует в мьютексе. Это подразумевает барьер памяти, поэтому, когда он выполняется, компилятор получает указание гарантировать, что все записи в переменные, которые могут быть видны другим потокам, должны быть сброшены в память. - person jalf; 22.07.2015
comment
Здесь по-прежнему требуется ключевое слово Volatile, чтобы компилятор не кэшировал флаг вместо чтения значения из памяти. - person FaceBro; 04.10.2016
comment
@guardian: Нет, анализ зависимостей данных рассматривает барьер памяти как внешнюю функцию, которая могла бы изменить любую переменную, которая когда-либо была псевдонимом. (Зарегистрировать локальные переменные хранилища, адрес которых никогда не берется, на самом деле совершенно безопасно). Даже в однопоточном коде global_x = 5; extern_call(); cout << global_x; компилятор не может заменить его на cout << 5;, потому что extern_call() могло изменить значение. - person Ben Voigt; 08.12.2016
comment
@Thomson: В каких случаях рассмотрение volatile как глобального препятствия для переупорядочения компилятора может вызвать серьезную проблему с производительностью? Если внутри фрагмента кода все места, которые нуждаются в барьерах, явно отмечены, то разрешение компилятору переупорядочить операции в volatile может иметь некоторую выгоду, но у одного есть код, который использует volatile, но не настроен для использования барьеров, понятных компилятору. using опция обработки volatile как барьера упорядочивания компилятора позволит этому компилятору с пользой запустить такой код. - person supercat; 19.02.2017
comment
Вы говорите, что volatile не требуется - это означает, что барьер памяти будет принудительно читать из памяти, а не из кешированного значения регистра. Это правильно, что барьеры памяти заставляют чтение не кэшироваться? Или вы имели в виду это для многопоточного использования volatile, а не для использования регистра / ввода-вывода? - person iheanyi; 19.09.2017
comment
На современных процессорах (последние десять лет или около того) volatile даже не выполняет принудительное чтение или запись в память. Это просто заставляет ЦП вести себя так, как если бы чтение или запись были принудительно записаны в память для однопоточного кода. И это все, что требовалось стандарту, поскольку у него не было конкретных многопоточных семантических требований. - person David Schwartz; 13.05.2018
comment
@iheanyi: Единственными преимуществами volatile перед инстринсами, предполагающими, что внешние силы могут читать и / или записывать любой объект в любой момент времени, являются: (1) существующий код, который не включает в себя изменчивость и препятствия, может быть легче исправлен, в большинстве случаев, с помощью добавляя volatile, чем добавляя барьеры; (2) на некоторых платформах могут потребоваться специальные инструкции для изменчивого чтения / записи, которые отличаются от выполнения синхронизации, затем доступа, а затем еще одной синхронизации. Единственная разумная причина, которую я могу придумать для C89, чтобы не включать барьеры (поскольку любая реализация должна иметь возможность достичь требуемых ... - person supercat; 18.07.2018
comment
... семантика, независимо от того, потребуют ли они на самом деле использования барьеров для этого), заключается в том, что авторы ожидали, что реализации, в которых программистам могут потребоваться барьеры для различных целей, сделают volatile их предоставлением). Даже регистры ввода-вывода часто не нуждаются в такой точной семантике, как то, что может предоставить volatile. Во многих случаях важно, чтобы одна группа операций предшествовала другой, но порядок операций внутри каждой группы не имеет значения. - person supercat; 18.07.2018
comment
@DavidSchwartz И это все, что требовал стандарт, поскольку он не имел особых семантических требований к многопоточности. Что предъявляет особые семантические требования к многопоточности? У нормальных переменных это есть? Где это указано? Хорошо ли определена какая-либо программа МП? - person curiousguy; 15.11.2019
comment
@jalf все записи в переменные, которые могут быть видны другим потокам, должны быть сброшены в память Сброшены как? Куда? - person curiousguy; 15.11.2019
comment
@Nemo Тем не менее, volatile всегда бесполезен для многопоточного программирования Почему вы не можете использовать volatile для простого и быстрого флага? - person curiousguy; 15.11.2019
comment
@curiousguy Как говорится в этом ответе (вы его читали?), volatile не дает никаких гарантий относительно порядка операций с памятью. Строго говоря, это даже не дает гарантии атомарности. Для любого из них вам либо нужны атомики C ++ 11, либо какой-то механизм, зависящий от реализации ... И с любым из них вам не нужен volatile. - person Nemo; 15.11.2019
comment
@Nemo Строго говоря, он даже не дает гарантии атомарности Конечно, квалификация сама по себе не может сделать обычно неатомарную операцию теперь атомарной: изменчивая квалифицированная большая структура в C не будет читается или записывается одним asm instr, так как такого instr даже не существует. Но объединение volatile и типа, который, как известно, имеет атомарные загрузки и сохранения на всей арке (например, int), может быть полезно для MT. Volatile гарантирует, что переменная представлена ​​в соответствии с ABI, и мы знаем, какие операции являются атомарными, на основе специальных знаний архитектуры. Я должен был это разъяснить. - person curiousguy; 15.11.2019
comment
volatile не дает никаких гарантий относительно порядка операций с памятью Так почему же мы не можем использовать volatile для простых флагов, которые не имеют особых требований к порядку, но к которым нужно обращаться с максимальной эффективностью? - person curiousguy; 15.11.2019
comment
@curiousguy Для стандартов, которые не упоминают многопоточность, ничто не имеет особой семантики многопоточности. Чтобы найти семантику для многопоточности, вам нужно взглянуть на стандарт многопоточности, на который вы кодируете. Если он говорит, что volatile имеет особую многопоточную семантику, значит, это так. Если нет, то это не так, и было бы глупо полагаться на это. Достойные стандарты многопоточности объясняют, что такое потокобезопасность, например, обычные переменные. Было бы ужасно, если бы они заставляли кодеров гадать, не так ли? - person David Schwartz; 18.11.2019
comment
@DavidSchwartz Обратите внимание на стандарт многопоточности, на который вы пишете код. Мы не программируем в вакууме. Стандарты говорят, что все изменчивые операции являются частью наблюдаемой трассировки. Мы знаем целевую машину и знаем, например, что запись скаляра размера слова является атомарной на целевой машине. Итак, мы знаем, что переменная запись является атомарной, потому что это операция низкого уровня. Мы можем рассуждать на уровне asm, потому что volatile дает нам связь от абстрактного к реальной машине. Использование volatile для MT глупо, когда у нас есть подходящие примитивы, которые выполняют свою работу. - person curiousguy; 18.11.2019
comment
@curiousguy Я согласен с тем, что вы можете проводить рассуждения для конкретной платформы и проверять их. Глупо это делать, потому что это не обязательно, и вы очень рискуете ошибиться, но вы можете это сделать. На некоторых реализациях / платформах у вас может не быть выбора. К счастью, современные платформы и современные стандарты потоковой передачи обеспечивают четкую, гарантированную семантику, а такие предположения и риск - это дело прошлого. (Кроме того, с volatile вы можете понести ненужные накладные расходы, потому что вам также нужно получить и заплатить за стандартную семантику однопоточности.) - person David Schwartz; 18.11.2019
comment
@DavidSchwartz Я категорически не согласен. 1) Однопоточная семантика volatile подразумевает так мало, а временная запись имеет стоимость энергонезависимой записи в обычную (не регистровую) переменную, которая не может быть оптимизирована (например, многие операции с неавтоматическими переменными, которые могут иметь псевдонимы). и это не часто оптимизируется), и 2) атомики почти никогда не оптимизируются (даже самая очевидная избыточная операция) на практике в любом случае. 3) Спецификация атомики и памяти абсолютно непонятна и видимо это сугубо бессмысленный и логичный мусор. - person curiousguy; 18.11.2019
comment
4) Многие компиляторы, которые пытались правильно реализовать порядок потребления, ошибались в особых случаях, создавая неправильный код, что могло затруднить диагностику ошибок. 5) Спецификация потребления в любом случае безумна, поскольку вы можете потреблять, вызывая статическую функцию-член для объекта. 6) Даже простейший Q в SO re: MT в C / C ++ приводит к противоречивым ответам. 7) Непонятно, почему атомарное int строго определено, чем неатомное int. 8) Никто не знает, точно, как делать проверки программ для программ MT. - person curiousguy; 18.11.2019

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

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

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

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

Рассмотрим типичный блок кода ядра:

spin_lock(&the_lock);
do_something_on(&shared_data);
do_something_else_with(&shared_data);
spin_unlock(&the_lock);

Если весь код следует правилам блокировки, значение shared_data не может неожиданно измениться, пока удерживается the_lock. Любой другой код, который может захотеть поиграть с этими данными, будет ждать блокировки. Примитивы спин-блокировки действуют как барьеры памяти - они явно написаны для этого - это означает, что доступ к данным не будет оптимизирован для них. Таким образом, компилятор может подумать, что он знает, что будет в shared_data, но вызов spin_lock (), поскольку он действует как барьер памяти, заставит его забыть все, что он знает. Проблем с оптимизацией доступа к этим данным не возникнет.

Если бы shared_data была объявлена ​​изменчивой, блокировка все равно была бы необходима. Но компилятор также не сможет оптимизировать доступ к shared_data внутри критического раздела, когда мы знаем, что никто другой не может с ним работать. Пока блокировка удерживается, shared_data не является изменчивым. При работе с общими данными правильная блокировка делает изменчивые данные ненужными - и потенциально опасными.

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

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

while (my_variable != what_i_want)
    cpu_relax();

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

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

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

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

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

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

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

person Community    schedule 21.03.2010
comment
Добавление ключевого слова volatile в операторы asm предотвратит это удаление. Действительно? - person curiousguy; 04.10.2011
comment
@curiousguy: Да. См. Также gcc.gnu.org/onlinedocs/gcc- 4.0.4 / gcc / Extended-Asm.html. - person Sebastian Mach; 22.12.2011
comment
Spin_lock () выглядит как обычный вызов функции. Особенность этого метода в том, что компилятор будет обрабатывать его особым образом, так что сгенерированный код забудет любое значение shared_data, которое было прочитано до spin_lock () и сохранено в регистре, так что значение должно быть прочитано заново в do_something_on ( ) после spin_lock ()? - person Joshua Chia; 29.03.2016
comment
@Syncopated Я почти уверен, что любое введение в блокировки / мьютексы и т. Д. Объяснит вам это. Но достаточно сказать, что ключевые слова присутствуют в тексте: барьер памяти. По сути, функции должны содержать определенный для платформы код операции или другой триггер, который, как известно компилятору, означает «вы абсолютно не можете перемещать какую-либо операцию между этим и следующим барьером памяти за пределами области, разделенной двумя». Затем, оказавшись внутри барьеров, мьютекс гарантирует, что никакой другой поток не может получить доступ к данным, и операции с shared_data происходят в установленном порядке, как это определяет Стандарт. - person underscore_d; 07.07.2016
comment
@underscore_d Я хочу сказать, что по имени функции spin_lock () я не могу сказать, что она делает что-то особенное. Я не знаю, что там. В частности, я не знаю, что в реализации мешает компилятору оптимизировать последующие чтения. - person Joshua Chia; 07.07.2016
comment
У Syncopated есть хорошая точка зрения. По сути, это означает, что программист должен знать внутреннюю реализацию этих специальных функций или, по крайней мере, быть очень хорошо осведомленным об их поведении. Это вызывает дополнительные вопросы, например - стандартизированы ли эти специальные функции и гарантированно ли они будут работать одинаково на всех архитектурах и во всех компиляторах? Доступен ли список таких функций или, по крайней мере, существует соглашение об использовании комментариев к коду, чтобы сообщить разработчикам, что данная функция защищает код от дальнейшей оптимизации? - person JustAMartin; 27.07.2016
comment
@Syncopated: дело не в том, что компилятор должен реализовывать специальную обработку, а в том, что компилятор должен сгенерировать код, который правильно работает с void spin_lock() { shared_data = 7; }, и единственный способ сделать это - выбросить копию, хранящуюся в регистре, и перечитать значение из памяти. Фактически, компилятор также должен сначала передать этот регистр обратно в память, чтобы void spin_lock() { if (shared_data == 42) abort(); } увидел значение. Итак, вы видите, что обращение с ним как с обычным вызовом функции на самом деле делает в компиляторе именно то, что нужно. - person Ben Voigt; 08.12.2016
comment
@BenVoigt Предположим, что shared_data - это приватная статика. Теперь компилятор знает, что spin_lock не может его коснуться. Так что никакого барьера памяти. А что будет, если добавить ограничение C99? - person Tuntable; 08.07.2017
comment
@Tuntable: частный статический объект может быть затронут любым кодом через указатель. И его адрес занимают. Возможно, анализ потока данных способен доказать, что указатель никогда не ускользнет, ​​но в целом это очень сложная проблема, сверхлинейная по размеру программы. Если у вас есть способ гарантировать, что псевдонимы не существуют, то перемещение доступа через спин-блокировку действительно должно быть нормальным. Но если псевдонимов не существует, volatile тоже бессмысленна. Во всех случаях вызов функции, тело которой не видно, будет правильным. - person Ben Voigt; 08.07.2017
comment
Согласно этому drdobbs.com/cpp/volatile-the-multithreaded -programmers-b /, компилятор может оптимизировать доступ к любой переменной в критическом разделе, защищенном мьютексом или блокировкой, поскольку такой критический раздел сериализуется и может рассматриваться как контекст одного потока. - person FaceBro; 20.09.2017
comment
@FaceBro: В этой статье признается концепция, согласно которой к объекту не нужно обращаться с использованием квалификатора volatile в тех случаях, когда он защищен мьютексом. Для автономной реализации простейшим видом конструкции мьютекса является флаг передачи токена, который указывает, кто владеет группой объектов, и каждый контекст использует этот флаг для передачи токена в любое время, когда он хочет, чтобы другой контекст использовал объект. В реализации, которая воздерживается от перемещения других операций после энергозависимой записи или до энергозависимого чтения, об этом может позаботиться единственный изменчивый флаг. - person supercat; 18.07.2018
comment
@underscore_d По сути, функции должны содержать определенный для платформы код операции или другой триггер. Какой конкретный код операции требуется на x86 для освобождения спин-блокировки? - person curiousguy; 14.11.2019
comment
@Tuntable Теперь компилятор знает, что spin_lock не может его коснуться. Что трогает? - person curiousguy; 14.11.2019

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

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

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

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

person Jeremy Friesner    schedule 20.03.2010
comment
Ваш логический флаг, вероятно, небезопасен. Как вы гарантируете, что рабочий выполнит свою задачу и что флаг останется в области видимости до тех пор, пока он не будет прочитан (если он прочитан)? Это работа для сигналов. Volatile хорош для реализации простых спин-блокировок , если не задействован мьютекс, поскольку безопасность псевдонимов означает, что компилятор предполагает, что mutex_lock (и любая другая библиотечная функция) может изменить состояние переменной-флага. - person Potatoswatter; 21.03.2010
comment
Очевидно, это работает, только если природа подпрограммы рабочего потока такова, что гарантировано периодически проверять логическое значение. Флаг volatile-bool-flag гарантированно останется в области видимости, потому что последовательность завершения потока всегда происходит до того, как объект, содержащий volatile-boolean, будет уничтожен, а последовательность завершения потока вызывает pthread_join () после установки значения bool. pthread_join () будет блокироваться до тех пор, пока рабочий поток не уйдет. У сигналов есть свои проблемы, особенно когда они используются в сочетании с многопоточностью. - person Jeremy Friesner; 21.03.2010
comment
Извините, я имею в виду сигналы pthread (переменные состояния), а не сигналы POSIX. Вы до сих пор не объяснили, как исполнитель гарантированно завершит свою работу до того, как логическое значение станет истинным. Предположительно он должен получать рабочие единицы в критическом разделе, иначе работа может быть запрошена после установки флага. Вы описали полусинхронную блокировку, которая может сработать для вас, но я бы не назвал ее шаблоном проектирования, и она, вероятно, не имеет преимуществ перед более безопасным, более традиционным механизмом. - person Potatoswatter; 21.03.2010
comment
Рабочий поток не гарантированно завершит свою работу до того, как логическое значение станет истинным - на самом деле, он почти наверняка будет в середине рабочего блока, если для логического значения установлено значение true. Но не имеет значения, когда рабочий поток завершит свою работу, потому что основной поток в любом случае не будет делать ничего, кроме блокировки внутри pthread_join (), пока рабочий поток не завершится. Таким образом, последовательность выключения хорошо упорядочена - изменчивый логический элемент (и любые другие общие данные) не будет освобожден до тех пор, пока не вернется pthread_join (), а pthread_join () не вернется, пока рабочий поток не исчезнет. - person Jeremy Friesner; 21.03.2010
comment
@ Джереми, вы правы на практике, но теоретически все еще может сломаться. В двухъядерной системе одно ядро ​​постоянно выполняет ваш рабочий поток. Другое ядро ​​устанавливает для bool значение true. Однако нет гарантии, что ядро ​​рабочего потока когда-либо увидит это изменение, т.е. оно может никогда не остановиться, даже если оно неоднократно проверяет логическое значение. Такое поведение допускается моделями памяти C ++ 0x, java и C #. На практике этого никогда не произойдет, поскольку занятый поток, скорее всего, вставит где-то барьер памяти, после чего он увидит изменение в логическом значении. - person deft_code; 22.03.2010
comment
@Caspin спасибо за эту информацию, это полезно знать. Я думал, что ключевое слово volatile решит проблемы с кэшированием многоядерной памяти. - person Jeremy Friesner; 22.03.2010
comment
@deft_code В двухъядерной системе одно ядро ​​постоянно выполняет ваш рабочий поток. А ОС никогда ничего не делает? - person curiousguy; 04.10.2011
comment
@curiousguy: Вы не можете делать никаких предположений о том, что ОС что-то делает. У вас может быть поток, работающий в соответствии с политикой планирования в реальном времени и с высоким приоритетом, и, следовательно, очень редко контекст переключается с одного из ваших ядер (подумайте о 16-ядерной системе, которая относительно не очень занята, а ваш рабочий поток выполняет вычисления и никогда / редко делает системные вызовы). ОС иногда что-то делает. Такое мышление заставляет вас думать очень сложными способами, чтобы убедиться, что вы разумно правы в своем конкретном случае. - person FooF; 31.07.2013
comment
@FooF Вы имеете в виду, что вычислительный поток с 0 системными вызовами может получить 100% процессорного времени без прерываний? Возможно теоретически, но не очень правдоподобно. В этом случае все, что вам нужно, - это отправить потоку асинхронный сигнал. - person curiousguy; 31.07.2013
comment
Возьмите систему POSIX, используйте политику планирования в реальном времени SCHED_FIFO, более высокий статический приоритет, чем у других процессов / потоков в системе, должно быть вполне возможно достаточное количество ядер. В Linux вы можете указать, что процесс реального времени может использовать 100% процессорного времени. Они никогда не будут переключать контекст, если нет потока / процесса с более высоким приоритетом, и никогда не блокируются вводом-выводом. Но дело в том, что C / C ++ volatile не предназначен для обеспечения надлежащей семантики совместного использования / синхронизации данных. Я считаю, что искать особые случаи, чтобы доказать, что неправильный код иногда может работать, - бесполезное занятие. - person FooF; 01.08.2013
comment
@Deft_code: как вы думаете, почему рабочий поток никогда не увидит изменения? Это потому, что рабочий поток не читает переменную или потому что поток супервизора не записывает переменную? Какому потоку нужен барьер памяти? - person David Grayson; 11.09.2015

volatile полезен (хотя и недостаточен) для реализации базовой конструкции мьютекса спин-блокировки, но как только он у вас есть (или что-то более качественное), вам больше не понадобится volatile.

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

pthread_mutex_t flag_guard_mutex; // contains something volatile
bool my_shared_flag;

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

Теперь у вас есть что-то вроде этого:

pthread_mutex_lock( &flag_guard_mutex );
my_local_state = my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

pthread_mutex_lock( &flag_guard_mutex ); // may alter my_shared_flag
my_shared_flag = ! my_shared_flag; // critical section
pthread_mutex_unlock( &flag_guard_mutex );

my_shared_flag не обязательно должен быть изменчивым, несмотря на то, что он не кэшируется, потому что

  1. Другой поток имеет к нему доступ.
  2. Meaning a reference to it must have been taken sometime (with the & operator).
    • (Or a reference was taken to a containing structure)
  3. pthread_mutex_lock - это библиотечная функция.
  4. Это означает, что компилятор не может определить, получает ли pthread_mutex_lock эту ссылку.
  5. Это означает, что компилятор должен предполагать, что pthread_mutex_lock изменяет общий флаг!
  6. Значит, переменную нужно перезагрузить из памяти. volatile, хотя и имеет значение в этом контексте, является посторонним.
person Potatoswatter    schedule 20.03.2010

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

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

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

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

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

atomic_t counter;

...
atomic_inc(&counter);

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

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

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

atomically {
  counter+=2;
}

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

person jpalecek    schedule 20.03.2010

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

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

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

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

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

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

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

Для более глубокого (хотя и не беспристрастного) обсуждения этого вопроса прочтите это

person zebrabox    schedule 21.03.2010
comment
+1 - гарантированная атомарность была еще одной частью того, чего мне не хватало. Я предполагал, что загрузка int является атомарной, поэтому volatile, предотвращающий переупорядочение, обеспечивает полное решение на стороне чтения. Я думаю, что это приличное предположение для большинства архитектур, но это не гарантия. - person Michael Ekstrand; 21.03.2010
comment
Когда отдельные операции чтения и записи в память являются прерываемыми и не атомарными? Есть ли польза? - person batbrat; 26.09.2019

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

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

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

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

Так что да, это правда, что компилятор, который строго (но очень агрессивно) соответствует ANSI C, может не работать с несколькими потоками без volatile. Но лучше кому-нибудь это исправить. Потому что любая СИСТЕМА (то есть, прагматически комбинация ядра, библиотек и компилятора C), которая не обеспечивает гарантии когерентности памяти POSIX, НЕ СООТВЕТСТВУЕТ стандарту POSIX. Период. Система НЕ МОЖЕТ требовать, чтобы вы использовали volatile для общих переменных для правильного поведения, потому что POSIX требует только, чтобы функции синхронизации POSIX были необходимы.

Так что, если ваша программа ломается из-за того, что вы не использовали volatile, это ОШИБКА. Это может быть не ошибка в C, или ошибка в библиотеке потоков, или ошибка в ядре. Но это ошибка СИСТЕМЫ, и один или несколько из этих компонентов должны работать, чтобы исправить ее.

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

/ --- [Дэйв Бутенхоф] ----------------------- [[email protected]] --- \
| Корпорация цифрового оборудования 110 Spit Brook Rd ZKO2-3 / Q18 |
| 603.881.2218, FAX 603.881.0120 Nashua NH 03062-2698 |
----------------- [Лучшая жизнь за счет параллелизма] ---------- ------ /

Г-н Бутенхоф во многом освещает те же вопросы в это сообщение usenet:

Использование «volatile» недостаточно для обеспечения надлежащей видимости памяти или синхронизации между потоками. Достаточно использования мьютекса, и, за исключением использования различных непереносимых альтернатив машинного кода (или более тонких последствий правил памяти POSIX, которые гораздо сложнее применять в целом, как объяснялось в моем предыдущем посте), мьютекс НЕОБХОДИМ.

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

Все это в равной степени применимо и к C ++.

person Tony Delroy    schedule 05.10.2010
comment
Ссылка не работает; он больше не указывает на то, что вы хотели процитировать. Без текста это бессмысленный ответ. - person jww; 15.08.2016

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

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

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

person Zack Yezek    schedule 02.08.2014

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

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

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

person david    schedule 14.11.2014
comment
Старые примеры требовали, чтобы программа обрабатывалась качественными компиляторами, подходящими для низкоуровневого программирования. К сожалению, современные компиляторы восприняли тот факт, что Стандарт не требует, чтобы они обрабатывали изменчивые файлы полезным образом, как указание на то, что код, который потребовал бы от них этого, сломан, вместо того, чтобы признать, что Стандарт не делает никаких попыток запретить реализации. которые соответствуют, но такого низкого качества, чтобы быть бесполезными, но никоим образом не оправдывают низкокачественные, но соответствующие компиляторы, которые стали популярными - person supercat; 18.07.2018
comment
На большинстве платформ было бы довольно легко распознать, что volatile нужно сделать, чтобы можно было написать ОС способом, который зависит от оборудования, но не зависит от компилятора. Требование, чтобы программисты использовали функции, зависящие от реализации, вместо того, чтобы заставлять volatile работать должным образом, подрывает цель создания стандарта. - person supercat; 18.07.2018