Когда использовать volatile с многопоточностью? - PullRequest
115 голосов
/ 30 декабря 2010

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

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

Ответы [ 4 ]

151 голосов
/ 30 декабря 2010

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

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

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

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

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

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

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

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

[/ End update]

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

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

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

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

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

29 голосов
/ 30 декабря 2010

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

/* global */ bool flag = false;

while (!flag) {}

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

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

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

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

0 голосов
/ 11 июля 2018
#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, он работает нормально, а если нет, то зацикливается.Пожалуйста, поправьте меня, если я неправ.

0 голосов
/ 31 декабря 2010

Вам требуется volatile и, возможно, блокировка.

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

volatile bool flag = false;

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

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

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

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

...