Летучие в С++ 11

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

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

EDIT: для тех, кто не знает этот пример (я серьезно удивлен), пожалуйста, прочтите следующее: https://en.wikipedia.org/wiki/Volatile_variable

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

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

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

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

Ответ 1

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

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

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

О, ваш код может работать. Но С++ 11 не гарантирует этого.

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

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

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

В разделе для разработчиков Intel говорится: "Волатильность: практически бесполезна для многопоточного программирования"

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

#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';
}