Ответ 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 для синхронизации требует вызова функции. Это так происходит, чтобы вызвать барьер памяти.