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

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

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

Ответ 1

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

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

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

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

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

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

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

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

[/Окончательное обновление]

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

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

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

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

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

Ответ 2

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

/* global */ bool flag = false;

while (!flag) {}

оптимизируется с помощью gcc:

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

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

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

Ответ 3

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

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

volatile bool flag = false;

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

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

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

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

Ответ 4

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