Что volatile
делает:
- Запрещает компилятору оптимизировать любой доступ. Каждое чтение / запись приводит к инструкции чтения / записи.
- Запрещает компилятору переупорядочивать доступ с другими значениями .
Что volatile
не делает:
- Сделать доступ атомарным.
- Предотвратить переупорядочивание компилятора с помощью энергонезависимого доступа.
- Внести изменения из одного потока в другой поток.
Некоторые непереносимые поведения, на которые не следует полагаться в кроссплатформенном C ++:
- VC ++ расширил
volatile
, чтобы предотвратить любое изменение порядка с другими инструкциями. Другие компиляторы этого не делают, потому что это отрицательно влияет на оптимизацию.
- x86 делает выравниваемое чтение / запись переменных размером с указатель и более мелкими атомарными и сразу же видимыми для других потоков. Другие архитектуры этого не делают.
Чаще всего люди действительно хотят получить заборы (также называемые барьерами) и атомарные инструкции, которые можно использовать, если у вас есть компилятор C ++ 11 или через компилятор и архитектуру. -зависимые функции в противном случае.
Заборы гарантируют, что в момент использования все предыдущие чтения / записи будут завершены. В C ++ 11 заборы управляются в различных точках с использованием перечисления std::memory_order
. В VC ++ вы можете использовать _ReadBarrier()
, _WriteBarrier()
и _ReadWriteBarrier()
для этого. Я не уверен насчет других компиляторов.
На некоторых архитектурах, таких как x86, забор - это просто способ помешать компилятору переупорядочивать инструкции. В других случаях они могут фактически выдать инструкцию, чтобы не дать процессору переупорядочить вещи.
Вот пример неправильного использования:
int res1, res2;
volatile bool finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished = true;
}
void spinning_thread()
{
while(!finished); // spin wait for res to be set.
}
Здесь finished
может быть переупорядочено на до того, как будет установлено либо res
! Ну, volatile предотвращает переупорядочение с другими volatile, верно? Давайте попробуем сделать каждый res
изменчивым тоже:
volatile int res1, res2;
volatile bool finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished = true;
}
void spinning_thread()
{
while(!finished); // spin wait for res to be set.
}
Этот тривиальный пример будет работать на x86, но он будет неэффективным. С одной стороны, это заставляет res1
быть установленным до res2
, даже если мы на самом деле не заботимся об этом ... мы просто хотим, чтобы они оба были установлены до finished
. Применение этого порядка между res1
и res2
будет препятствовать только действительным оптимизациям, снижая производительность.
Для более сложных задач вам придется делать каждые записи volatile
. Это приведет к тому, что ваш код будет раздуваться, будет очень подвержен ошибкам и станет медленным, поскольку он предотвращает гораздо большее изменение порядка, чем вы действительно хотели.
Это не реально. Поэтому мы используем заборы и атомные. Они допускают полную оптимизацию и гарантируют, что доступ к памяти будет завершен в точке забора:
int res1, res2;
std::atomic<bool> finished;
void work_thread(int a, int b)
{
res1 = a + b;
res2 = a - b;
finished.store(true, std::memory_order_release);
}
void spinning_thread()
{
while(!finished.load(std::memory_order_acquire));
}
Это будет работать для всех архитектур. Операции res1
и res2
могут быть переупорядочены по усмотрению компилятора. Выполнение атомарного релиза гарантирует, что все неатомарные операции приказаны завершить и быть видимыми для потоков, которые выполняют атомарный приобретают .