Неправильное ключевое слово C ++ вовлекает забор памяти?

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

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


Есть интересная дискуссия по этому связанному вопросу

Джонатан Вакели пишет :

… Доступ к отдельным изменчивым переменным не может быть переупорядочен компилятором до тех пор, пока они встречаются в отдельных полных выражениях … правильно, что волатильность бесполезна для безопасности streamов, но не по причинам, которые он дает. Это связано не только с тем, что компилятор может переупорядочить обращения к изменчивым объектам, а потому, что CPU может их переупорядочить. Атомные операции и барьеры памяти препятствуют переупорядочению компилятора и процессора

На что Дэвид Шварц отвечает в комментариях :

… С точки зрения стандарта C ++ нет никакой разницы между компилятором, который делает что-то, и инструкциями по выпуску компилятора, которые заставляют аппаратное обеспечение что-то делать. Если ЦП может переупорядочить обращения к летучим, то стандарт не требует сохранения их порядка. …

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

Я хочу сказать, что если стандарт C ++ запрещает компилятору переупорядочивать обращения к отдельным летучим, то теория о том, что порядок таких доступов является частью наблюдаемого поведения программы, также требует от компилятора испускать код, который запрещает ЦП так. Стандарт не делает различий между тем, что делает компилятор, и тем, что делает код компилятора для ЦП.

Что дает два вопроса: является ли одно из них «правильным»? Что действительно делают реальные реализации?

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

  • Когда внутри обработчика сигнала. Поскольку запись в volatile переменную – это единственное, что стандарт позволяет вам делать из обработчика сигнала. Начиная с C ++ 11, вы можете использовать std::atomic для этой цели, но только если атом блокирован.
  • При работе с setjmp соответствии с Intel .
  • При непосредственном взаимодействии с оборудованием и хотите убедиться, что компилятор не оптимизирует ваши чтения или записи.

Например:

 volatile int *foo = some_memory_mapped_device; while (*foo) ; // wait until *foo turns false 

Без volatile спецификатора компилятору разрешается полностью оптимизировать цикл. Спецификатор volatile сообщает компилятору, что он не может предположить, что 2 последующих чтения возвращают одно и то же значение.

Обратите внимание, что volatile имеет ничего общего с streamами. Вышеприведенный пример не работает, если на *foo другая нить, потому что нет операции по приобретению.

Во всех других случаях использование volatile следует рассматривать как не переносимый, а не обзор кода прохода, за исключением случаев, когда речь идет о компиляторах pre-C ++ 11 и расширениях компилятора (таких как переключатель msvc /volatile:ms , который включен по умолчанию в X86 / I64).

Неправильное ключевое слово C ++ вовлекает забор памяти?

Компилятор C ++, который соответствует спецификации, не обязан вводить забор памяти. Ваш конкретный компилятор может; направьте свой вопрос авторам своего компилятора.

Функция «volatile» в C ++ не имеет ничего общего с streamовой обработкой. Помните, что цель «volatile» – отключить оптимизацию компилятора, так что чтение из регистра, которое изменяется из-за экзогенных условий, не оптимизировано. Является ли адрес памяти, который записывается другим streamом на другом процессоре, регистр, который изменяется из-за экзогенных условий? Нет. Опять же, если некоторые авторы компилятора решили обрабатывать адреса памяти, написанные разными streamами на разных ЦП, как если бы они менялись из-за экзогенных условий, это их дело; они не обязаны это делать. Они также не требуются – даже если они вводят забор памяти – например, чтобы гарантировать, что каждый stream видит последовательный порядок волатильных чтений и записи.

Фактически, volatile практически бесполезен для streamовой обработки в C / C ++. Лучшая практика – это избежать этого.

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

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

То, что Дэвид упускает из виду, – это тот факт, что стандарт c ++ определяет поведение нескольких streamов, взаимодействующих только в определенных ситуациях, а все остальное приводит к неопределенному поведению. Условие гонки, включающее хотя бы одну запись, не определено, если вы не используете атомные переменные.

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

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

Что касается реализации волатильных доступов, это выбор компилятора.

Эта статья , описывающая поведение gcc, показывает, что вы не можете использовать изменчивый объект в качестве барьера памяти для упорядочивания последовательности записей в энергозависимую память.

Что касается поведения icc, я обнаружил, что этот источник сообщает также, что volatile не гарантирует упорядочение доступа к памяти.

Компилятор Microsoft VS2013 имеет другое поведение. В этой документации объясняется, как volatile обеспечивает семантику Release / Acquire и позволяет использовать изменчивые объекты в блокировках / релизах в многопоточных приложениях.

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

Поэтому мой ответ на:

Неправильное ключевое слово C ++ вовлекает забор памяти?

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

Насколько я знаю, компилятор только вставляет забор памяти в архитектуре Itanium.

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

Это зависит от того, какой компилятор «компилятор». Visual C ++ работает с 2005 года. Но стандарт не требует этого, поэтому некоторые другие компиляторы этого не делают.

Это в основном из памяти и на основе pre-C ++ 11 без streamов. Но, участвуя в дискуссиях по теме в комитете, я могу сказать, что комитет никогда не думал о том, что volatile может использоваться для синхронизации между streamами. Microsoft предложила его, но это предложение не было выполнено.

Ключевая спецификация volatile заключается в том, что доступ к volatile представляет собой «наблюдаемое поведение», как и IO. Точно так же компилятор не может переупорядочить или удалить определенный IO, он не может переупорядочивать или удалять обращения к изменчивому объекту (или, вернее, обращается через выражение lvalue с летучим квалифицированным типом). Первоначальное намерение волатильности было, по сути, поддерживать память с отображением IO. «Проблема» с этим, однако, заключается в том, что именно реализация определяется, что представляет собой «неустойчивый доступ». И многие компиляторы реализуют его так, как будто определение было «инструкция, которая считывает или записывает в память». Это законное, хотя и бесполезное определение, если реализация указывает его. (Я еще не нашел фактическую спецификацию для любого компилятора.)

Возможно (и это аргумент, который я принимаю), это нарушает цель стандарта, поскольку, если аппаратное обеспечение не распознает адреса как отображаемое в памяти IO и не блокирует любое переупорядочение и т. Д., Вы даже не можете использовать volatile для IO с отображением памяти, по крайней мере, на архитектурах Sparc или Intel. Тем не менее, ни один из коммиллеров, на которые я смотрел (Sun CC, g ++ и MSC), не выводит никаких инструкций по заграждению или membar. (Примерно в то время, когда Microsoft предлагала распространять правила для volatile , я думаю, что некоторые из их компиляторов внедрили свое предложение и выпустили инструкции для забора волатильных доступов. Я не проверял, что делают последние компиляторы, но меня это не удивит, если это зависит от некоторых параметров компилятора. Однако версия I checkd – я думаю, что это был VS6.0-не испускал заборы.)

Это не обязательно. Volatile – не примитив синхронизации. Он просто отключает оптимизацию, т. Е. Вы получаете предсказуемую последовательность чтения и записи в streamе в том же порядке, который предписывается абстрактной машиной. Но чтение и запись в разных streamах не имеют порядка, в первую очередь, нет смысла говорить о сохранении или не сохранении их порядка. Порядок между адами может быть установлен примитивами синхронизации, вы получаете UB без них.

Немного разъяснения относительно барьеров памяти. Типичный процессор имеет несколько уровней доступа к памяти. Существует конвейер памяти, несколько уровней кеша, затем RAM и т. Д.

Инструкции Membar смывают трубопровод. Они не меняют порядок, в котором выполняются чтения и записи, а просто заставляют выдающиеся выполнять в данный момент. Это полезно для многопоточных программ, но не так много.

Кэш (ы) обычно автоматически согласованы между CPU. Если вы хотите убедиться, что кеш находится в синхронизации с ОЗУ, необходим кеш-флеш. Он очень отличается от мембара.

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

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

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

Я делаю это для ОЗУ, а также для ввода IO с памятью.

Основываясь на обсуждении здесь, кажется, что это по-прежнему действительное использование volatile, но не имеет ничего общего с несколькими streamами или процессорами. Если компилятор для микроcontrollerа «знает», что не может быть никаких других доступов (например, каждый из них встроен в чип, без кеша и есть только одно kernel), я думаю, что забор памяти вообще не подразумевается, компилятор просто нужно предотвратить определенные оптимизации.

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

Я думаю, что путаница вокруг изменчивости и переупорядочения команд проистекает из двух понятий переупорядочения процессоров:

  1. Исполнение вне порядка.
  2. Последовательность чтения / записи памяти, как видно из других процессоров (переупорядочение в том смысле, что каждый процессор может видеть другую последовательность).

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

Внештатное исполнение

ЦП могут выполнять инструкции не по порядку / спекулятивно при условии, что конечный результат мог произойти в исходном коде. Процессоры могут выполнять преобразования, которые запрещены в компиляторах, потому что компиляторы могут выполнять только те преобразования, которые являются правильными при любых обстоятельствах. Напротив, процессоры могут проверять правильность этих оптимизаций и возвращаться из них, если они оказываются неверными.

Последовательность чтения / записи в памяти, как видно из других процессоров

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

Источники:

  • Блокировка памяти
  • LLVM: Atomics
  • ACCESS_ONCE () и compiler errors

В то время как я работал с онлайн-загружаемым видеоуроком для разработки 3D Graphics & Game Engine, работающего с современным OpenGL. Мы использовали volatile в одном из наших classов. Веб-сайт учебника можно найти здесь, и видео, работающее с ключевым словом volatile находится в видео 98 серии Shader Engine . Эти работы не являются моими, но аккредитованы в Marek A. Krzeminski, MASc и это отрывок из видео загрузите страницу.

«Поскольку теперь мы можем запускать наши игры в нескольких streamах, важно правильно синхронизировать данные между streamами. В этом видео я покажу, как создать class волатильной блокировки для обеспечения правильной синхронизации волатильных переменных …»

И если вы подписаны на его сайт и имеете доступ к своим видео в этом видео, он ссылается на эту статью, касающуюся использования Volatile с multithreading программированием.

Вот статья по ссылке выше: http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

volatile: лучший друг многопоточного программиста

1 февраля 2001 года Андрей Александреску

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

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

Программы, использующие несколько streamов, как правило, трудно писать, доказывать правильность, отладку, поддержку и приручение в целом. Некорректные многопоточные программы могут работать в течение многих лет без сбоев, только для неожиданного запуска amok, потому что некоторые критические условия синхронизации были выполнены.

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

Просто небольшое ключевое слово

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

Как и его более известный аналог const, volatile является модификатором типа. Он предназначен для использования в сочетании с переменными, которые доступны и модифицируются в разных streamах. Basically, without volatile, either writing multithreaded programs becomes impossible, or the compiler wastes vast optimization opportunities. An explanation is in order.

Рассмотрим следующий код:

 class Gadget { public: void Wait() { while (!flag_) { Sleep(1000); // sleeps for 1000 milliseconds } } void Wakeup() { flag_ = true; } ... private: bool flag_; }; 

The purpose of Gadget::Wait above is to check the flag_ member variable every second and return when that variable has been set to true by another thread. At least that’s what its programmer intended, but, alas, Wait is incorrect.

Suppose the compiler figures out that Sleep(1000) is a call into an external library that cannot possibly modify the member variable flag_. Then the compiler concludes that it can cache flag_ in a register and use that register instead of accessing the slower on-board memory. This is an excellent optimization for single-threaded code, but in this case, it harms correctness: after you call Wait for some Gadget object, although another thread calls Wakeup, Wait will loop forever. This is because the change of flag_ will not be reflected in the register that caches flag_. The optimization is too … optimistic.

Caching variables in registers is a very valuable optimization that applies most of the time, so it would be a pity to waste it. C and C++ give you the chance to explicitly disable such caching. If you use the volatile modifier on a variable, the compiler won’t cache that variable in registers — each access will hit the actual memory location of that variable. So all you have to do to make Gadget’s Wait/Wakeup combo work is to qualify flag_ appropriately:

 class Gadget { public: ... as above ... private: volatile bool flag_; }; 

Most explanations of the rationale and usage of volatile stop here and advise you to volatile-qualify the primitive types that you use in multiple threads. However, there is much more you can do with volatile, because it is part of C++’s wonderful type system.

Using volatile with User-Defined Types

You can volatile-qualify not only primitive types, but also user-defined types. In that case, volatile modifies the type in a way similar to const. (You can also apply const and volatile to the same type simultaneously.)

Unlike const, volatile discriminates between primitive types and user-defined types. Namely, unlike classes, primitive types still support all of their operations (addition, multiplication, assignment, etc.) when volatile-qualified. For example, you can assign a non-volatile int to a volatile int, but you cannot assign a non-volatile object to a volatile object.

Let’s illustrate how volatile works on user-defined types on an example.

 class Gadget { public: void Foo() volatile; void Bar(); ... private: String name_; int state_; }; ... Gadget regularGadget; volatile Gadget volatileGadget; 

If you think volatile is not that useful with objects, prepare for some surprise.

 volatileGadget.Foo(); // ok, volatile fun called for // volatile object regularGadget.Foo(); // ok, volatile fun called for // non-volatile object volatileGadget.Bar(); // error! Non-volatile function called for // volatile object! 

The conversion from a non-qualified type to its volatile counterpart is trivial. However, just as with const, you cannot make the trip back from volatile to non-qualified. You must use a cast:

 Gadget& ref = const_cast(volatileGadget); ref.Bar(); // ok 

A volatile-qualified class gives access only to a subset of its interface, a subset that is under the control of the class implementer. Users can gain full access to that type’s interface only by using a const_cast. In addition, just like constness, volatileness propagates from the class to its members (for example, volatileGadget.name_ and volatileGadget.state_ are volatile variables).

volatile, Critical Sections, and Race Conditions

The simplest and the most often-used synchronization device in multithreaded programs is the mutex. A mutex exposes the Acquire and Release primitives. Once you call Acquire in some thread, any other thread calling Acquire will block. Later, when that thread calls Release, precisely one thread blocked in an Acquire call will be released. In other words, for a given mutex, only one thread can get processor time in between a call to Acquire and a call to Release. The executing code between a call to Acquire and a call to Release is called a critical section. (Windows terminology is a bit confusing because it calls the mutex itself a critical section, while “mutex” is actually an inter-process mutex. It would have been nice if they were called thread mutex and process mutex.)

Mutexes are used to protect data against race conditions. By definition, a race condition occurs when the effect of more threads on data depends on how threads are scheduled. Race conditions appear when two or more threads compete for using the same data. Because threads can interrupt each other at arbitrary moments in time, data can be corrupted or misinterpreted. Consequently, changes and sometimes accesses to data must be carefully protected with critical sections. In object-oriented programming, this usually means that you store a mutex in a class as a member variable and use it whenever you access that class’ state.

Experienced multithreaded programmers might have yawned reading the two paragraphs above, but their purpose is to provide an intellectual workout, because now we will link with the volatile connection. We do this by drawing a parallel between the C++ types’ world and the threading semantics world.

  • Outside a critical section, any thread might interrupt any other at any time; there is no control, so consequently variables accessible from multiple threads are volatile. This is in keeping with the original intent of volatile — that of preventing the compiler from unwittingly caching values used by multiple threads at once.
  • Inside a critical section defined by a mutex, only one thread has access. Consequently, inside a critical section, the executing code has single-threaded semantics. The controlled variable is not volatile anymore — you can remove the volatile qualifier.

In short, data shared between threads is conceptually volatile outside a critical section, and non-volatile inside a critical section.

You enter a critical section by locking a mutex. You remove the volatile qualifier from a type by applying a const_cast. If we manage to put these two operations together, we create a connection between C++’s type system and an application’s threading semantics. We can make the compiler check race conditions for us.

LockingPtr

We need a tool that collects a mutex acquisition and a const_cast. Let’s develop a LockingPtr class template that you initialize with a volatile object obj and a mutex mtx. During its lifetime, a LockingPtr keeps mtx acquired. Also, LockingPtr offers access to the volatile-stripped obj. The access is offered in a smart pointer fashion, through operator-> and operator*. The const_cast is performed inside LockingPtr. The cast is semantically valid because LockingPtr keeps the mutex acquired for its lifetime.

First, let’s define the skeleton of a class Mutex with which LockingPtr will work:

 class Mutex { public: void Acquire(); void Release(); ... }; 

To use LockingPtr, you implement Mutex using your operating system’s native data structures and primitive functions.

LockingPtr is templated with the type of the controlled variable. For example, if you want to control a Widget, you use a LockingPtr that you initialize with a variable of type volatile Widget.

LockingPtr’s definition is very simple. LockingPtr implements an unsophisticated smart pointer. It focuses solely on collecting a const_cast and a critical section.

 template  class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast(&obj)), pMtx_(&mtx) { mtx.Lock(); } ~LockingPtr() { pMtx_->Unlock(); } // Pointer behavior T& operator*() { return *pObj_; } T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); }; 

In spite of its simplicity, LockingPtr is a very useful aid in writing correct multithreaded code. You should define objects that are shared between threads as volatile and never use const_cast with them — always use LockingPtr automatic objects. Let’s illustrate this with an example.

Say you have two threads that share a vector object:

 class SyncBuf { public: void Thread1(); void Thread2(); private: typedef vector BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ }; 

Inside a thread function, you simply use a LockingPtr to get controlled access to the buffer_ member variable:

 void SyncBuf::Thread1() { LockingPtr lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { ... use *i ... } } 

The code is very easy to write and understand — whenever you need to use buffer_, you must create a LockingPtr pointing to it. Once you do that, you have access to vector’s entire interface.

The nice part is that if you make a mistake, the compiler will point it out:

 void SyncBuf::Thread2() { // Error! Cannot access 'begin' for a volatile object BufT::iterator i = buffer_.begin(); // Error! Cannot access 'end' for a volatile object for ( ; i != lpBuf->end(); ++i ) { ... use *i ... } } 

You cannot access any function of buffer_ until you either apply a const_cast or use LockingPtr. The difference is that LockingPtr offers an ordered way of applying const_cast to volatile variables.

LockingPtr is remarkably expressive. If you only need to call one function, you can create an unnamed temporary LockingPtr object and use it directly:

 unsigned int SyncBuf::Size() { return LockingPtr(buffer_, mtx_)->size(); } 

Back to Primitive Types

We saw how nicely volatile protects objects against uncontrolled access and how LockingPtr provides a simple and effective way of writing thread-safe code. Let’s now return to primitive types, which are treated differently by volatile.

Let’s consider an example where multiple threads share a variable of type int.

 class Counter { public: ... void Increment() { ++ctr_; } void Decrement() { —ctr_; } private: int ctr_; }; 

If Increment and Decrement are to be called from different threads, the fragment above is buggy. First, ctr_ must be volatile. Second, even a seemingly atomic operation such as ++ctr_ is actually a three-stage operation. Memory itself has no arithmetic capabilities. When incrementing a variable, the processor:

  • Reads that variable in a register
  • Increments the value in the register
  • Writes the result back to memory

This three-step operation is called RMW (Read-Modify-Write). During the Modify part of an RMW operation, most processors free the memory bus in order to give other processors access to the memory.

If at that time another processor performs a RMW operation on the same variable, we have a race condition: the second write overwrites the effect of the first.

To avoid that, you can rely, again, on LockingPtr:

 class Counter { public: ... void Increment() { ++*LockingPtr(ctr_, mtx_); } void Decrement() { —*LockingPtr(ctr_, mtx_); } private: volatile int ctr_; Mutex mtx_; }; 

Now the code is correct, but its quality is inferior when compared to SyncBuf’s code. Зачем? Because with Counter, the compiler will not warn you if you mistakenly access ctr_ directly (without locking it). The compiler compiles ++ctr_ if ctr_ is volatile, although the generated code is simply incorrect. The compiler is not your ally anymore, and only your attention can help you avoid race conditions.

What should you do then? Simply encapsulate the primitive data that you use in higher-level structures and use volatile with those structures. Paradoxically, it’s worse to use volatile directly with built-ins, in spite of the fact that initially this was the usage intent of volatile!

volatile Member Functions

So far, we’ve had classes that aggregate volatile data members; now let’s think of designing classes that in turn will be part of larger objects and shared between threads. Here is where volatile member functions can be of great help.

When designing your class, you volatile-qualify only those member functions that are thread safe. You must assume that code from the outside will call the volatile functions from any code at any time. Don’t forget: volatile equals free multithreaded code and no critical section; non-volatile equals single-threaded scenario or inside a critical section.

For example, you define a class Widget that implements an operation in two variants — a thread-safe one and a fast, unprotected one.

 class Widget { public: void Operation() volatile; void Operation(); ... private: Mutex mtx_; }; 

Notice the use of overloading. Now Widget’s user can invoke Operation using a uniform syntax either for volatile objects and get thread safety, or for regular objects and get speed. The user must be careful about defining the shared Widget objects as volatile.

When implementing a volatile member function, the first operation is usually to lock this with a LockingPtr. Then the work is done by using the non- volatile sibling:

 void Widget::Operation() volatile { LockingPtr lpThis(*this, mtx_); lpThis->Operation(); // invokes the non-volatile function } 

Резюме

When writing multithreaded programs, you can use volatile to your advantage. You must stick to the following rules:

  • Define all shared objects as volatile.
  • Don’t use volatile directly with primitive types.
  • When defining shared classes, use volatile member functions to express thread safety.

If you do this, and if you use the simple generic component LockingPtr, you can write thread-safe code and worry much less about race conditions, because the compiler will worry for you and will diligently point out the spots where you are wrong.

A couple of projects I’ve been involved with use volatile and LockingPtr to great effect. The code is clean and understandable. I recall a couple of deadlocks, but I prefer deadlocks to race conditions because they are so much easier to debug. There were virtually no problems related to race conditions. But then you never know.

Acknowledgements

Many thanks to James Kanze and Sorin Jianu who helped with insightful ideas.


Andrei Alexandrescu is a Development Manager at RealNetworks Inc. (www.realnetworks.com), based in Seattle, WA, and author of the acclaimed book Modern C++ Design. He may be contacted at http://www.moderncppdesign.com. Andrei is also one of the featured instructors of The C++ Seminar (www.gotw.ca/cpp_seminar).

This article might be a little dated, but it does give good insight towards an excellent use of using the volatile modifier with in the use of multithreaded programming to help keep events asynchronous while having the compiler checking for race conditions for us. This may not directly answer the OPs original question about creating a memory fence, but I choose to post this as an answer for others as an excellent reference towards a good use of volatile when working with multithreaded applications.

The keyword volatile essentially means that reads and writes an object should be performed exactly as written by the program, and not optimized in any way . Binary code should follow C or C++ code: a load where this is read, a store where there is a write.

It also means that no read should be expected to result in a predictable value: the compiler shouldn’t assume anything about a read even immediately following a write to the same volatile object:

 volatile int i; i = 1; int j = i; if (j == 1) // not assumed to be true 

volatile may be the most important tool in the “C is a high level assembly language” toolbox .

Whether declaring an object volatile is sufficient for ensuring the behavior of code that deals with asynchronous changes depends on the platform: different CPU give different levels of guaranteed synchronization for normal memory reads and writes. You probably shouldn’t try to write such low level multithreading code unless you are an expert in the area.

Atomic primitives provide a nice higher level view of objects for multithreading that makes it easy to reason about code. Almost all programmers should use either atomic primitives or primitives that provide mutual exclusions like mutexes, read-write-locks, semaphores, or other blocking primitives.

  • Иллюстрирование использования ключевого слова volatile в C #
  • Почему в C требуется летучесть?
  • Давайте будем гением компьютера.