Летучий в C ++ 11

В стандарте C ++ 11 модель машины изменилась с однопоточной машины на многопоточную машину.

Означает ли это, что типичный static int x; void func() { x = 0; while (x == 0) {} } пример оптимизированного чтения больше не будет происходить в C ++ 11?

РЕДАКТИРОВАТЬ: для тех, кто не знает этот пример (я серьезно удивлен), пожалуйста, прочитайте это: https://en.wikipedia.org/wiki/Volatile_variable

EDIT2:
ОК, я действительно ожидал, что все, кто знал, что volatile Это видел этот пример.

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

Решение, конечно, заключается в использовании volatile что заставит компилятор читать переменную при каждом доступе.

Мой вопрос заключается в том, является ли это устаревшей проблемой в C ++ 11, поскольку модель машины является многопоточной, поэтому компилятор должен учитывать одновременный доступ к переменной, присутствующей в системе.

24

Решение

Будет ли он оптимизирован, полностью зависит от компиляторов и того, что они выберут для оптимизации. Модель памяти C ++ 98/03 не распознает возможность того, что x может измениться между его настройкой и извлечением значения.

Модель памяти C ++ 11 делает признать, что x может быть изменено Тем не мение, это не волнует. Неатомарный доступ к переменным (то есть: не использовать std::atomics или надлежащие взаимные исключения) приводит к неопределенному поведению. Поэтому для компилятора C ++ 11 вполне нормально предположить, что x никогда не меняется между записью и чтением, поскольку неопределенное поведение может означать, что «функция никогда не видит x изменить когда-либо. «

Теперь давайте посмотрим, что C ++ 11 говорит о volatile int x;, Если вы поместите это туда, и у вас возникнет какая-то другая нить x, у вас все еще есть неопределенное поведение. Летучий не влияет нарезания резьбы поведение. Модель памяти C ++ 11 не определяет чтение или запись из / в x быть атомарным, и при этом не требуется, чтобы барьеры памяти, необходимые для неатомарного чтения / записи, были правильно упорядочены. volatile не имеет к этому никакого отношения.

О, твой код может быть Работа. Но C ++ 11 не гарантировать это.

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

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

volatile гарантии ни один из этого. Volatile работает с «оборудованием, отображенной памятью и прочим», потому что оборудование, которое записывает эту память, гарантирует, что проблема с кешем решена. Если ядра ЦП выдавали барьер памяти после каждой записи, вы можете просто надеяться на прощание. Таким образом, в C ++ 11 есть особый язык, говорящий, когда конструкции должны создавать барьер.

volatile о памяти доступ (когда читать); потоки о памяти целостность (что на самом деле там хранится).

Модель памяти C ++ 11 определяет, какие операции приведут к тому, что записи в одном потоке станут видимыми в другом. Это о целостность памяти, что не то volatile ручки. А целостность памяти обычно требует, чтобы оба потока что-то делали.

Например, если поток A блокирует мьютекс, выполняет запись, а затем разблокирует ее, модель памяти C ++ 11 требует, чтобы запись стала видимой для потока B, если поток B впоследствии заблокирует ее. Пока это не приобретет конкретный блокировка, это не определено, какое значение там. Этот материал подробно описан в разделе 1.10 стандарта.

Давайте посмотрим на код, который вы цитируете, по отношению к стандарту. В разделе 1.10, p8 говорится о способности определенных библиотечных вызовов вызывать поток для синхронизации с другим потоком. В большинстве других параграфов объясняется, как синхронизация (и другие вещи) создает порядок операций между потоками. Конечно, твой код не вызывает ничего из этого. Там нет точки синхронизации, нет порядка упорядочения, ничего.

Без такой защиты, без какой-либо формы синхронизации или упорядочения, 1.10 p21 входит в:

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

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

Таким образом, вы достигли неопределенного поведения.

Так что единственный случай, когда вы получаете гарантированный многопоточное поведение в модели памяти C ++ 11, если вы используете правильный мьютекс или std::atomic<int> x с правильной атомной загрузкой / хранением вызовов.

О, и тебе не нужно делать x летучий тоже. Каждый раз, когда вы вызываете (не встроенную) функцию, эта функция или то, что она вызывает, может изменить глобальную переменную. Так что не могу оптимизировать прочтение x в while петля. И каждый механизм C ++ 11 для синхронизации требует вызова функции. Это как раз и вызывает барьер памяти.

69

Другие решения

«В соответствии со стандартом ISO C ++ 11 ключевое слово volatile предназначено только для доступа к оборудованию»

Ключевое слово volatile используется в этом примере обработчика сигнала из cppreference.com

#include <csignal>
#include <iostream>

namespace
{
volatile std::sig_atomic_t gSignalStatus;
}

void signal_handler(int signal)
{
gSignalStatus = signal;
}

int main()
{
// Install a signal handler
std::signal(SIGINT, signal_handler);

std::cout << "SignalValue: " << gSignalStatus << '\n';
std::cout << "Sending signal " << SIGINT << '\n';
std::raise(SIGINT);
std::cout << "SignalValue: " << gSignalStatus << '\n';
}
1

По вопросам рекламы [email protected]