Это правило о волатильном использовании строгое?
Я видел это предложение:
общее правило: если у вас есть переменные примитивного типа, которые должны совместно использоваться несколькими потоками, объявляйте эти переменные переменными
из этой статьи, и это предложение:
В общем, любые данные, которые могут быть не датированы асинхронно, должны быть объявлены как изменчивые.
На этой странице, рассматривая это введенное правило, я хотел бы знать, не могли бы вы привести пример случая, когда, несмотря на существование асинхронного доступа к данным, заявляющего, что данные volatile бесполезны на практике, или нет такого исключительного случая и правила? строгое
8 ответов
Я помню, когда эта статья была опубликована, и я помню бесконечные дискуссии, которые затем велись на comp.lang.C++.
IIRC, Андрей угоняет volatile
ключевое слово, чтобы использовать его для различения различных перегрузок функций. (См. Эту статью Скотта Мейерса для другой такой идеи.) То, что он делает, блестяще, так как позволяет компилятору ловить вас, если вы испортили защищенный и незащищенный доступ к объектам (очень похоже на то, как компилятор ловит вас, если вы попробуете) изменить константу). Но помимо того, что он помогает вам, он не имеет ничего общего с реальной защитой одновременного доступа к объектам.
Проблема только в том, что 90% людей имеют один взгляд на статью, и все, что они видят, это volatile
и "темы" в той же статье. В зависимости от своих знаний они либо делают неверный вывод, что volatile
это хорошо для темы (вы, кажется, так и сделали), или они кричат на него, заставляя других делать неправильные выводы.
Похоже, очень немногие люди действительно могут полностью прочитать статью и понять, что он на самом деле делает.
Я не могу говорить о реальном сценарии асинхронного доступа, так как я не слишком хорош в многопоточности, но что volatile
Модификатор это сказать компилятору:
"Послушай, это может измениться в любое время, так что не кешируй его, не записывай в реестр и не делай сумасшедших дел, ладно?"
Он не защищает от асинхронных записей, он просто отключает недопустимые оптимизации, если переменная может быть изменена внешними силами.
Редактировать: В качестве потенциального примера, который не включает многопоточность (но, действительно, включает в себя исключительно извилистый код;), вот случай, где volatile важна:
volatile bool keepRunning = true;
void Stuff() {
int notAPointer = 0;
notAPointer = (int)(&keepRunning); //Don't do this! Especially on 64-bit processors!
while(keepRunning) {
*(bool*)(notAPointer) = false;
}
printf("The loop terminated!");
}
Без этого изменчивого модификатора компилятор мог бы сказать: "Эй, keepRunning никогда не изменяется, поэтому мне даже не нужно генерировать код, который его проверяет!", Когда на самом деле мы просто модифицируем его в тайне.
(В действительности это, вероятно, все еще будет работать на неоптимизированной сборке. И это также может работать, если компилятор умный и заметит, что указатель взят. Но принцип тот же)
Прочитайте это. Volatile не имеет ничего общего с многопоточностью.
Я бы сказал, что в теории эти правила абсолютно верны, и переменная необходима каждый раз, когда к переменной обращаются 2 потока. (Даже при использовании мьютексов они не препятствуют оптимизации компилятора.) Но на практике компиляторы достаточно хорошо распознают ситуации, когда переменная может быть изменена вне конкретной функции, чтобы ее значение не кэшировалось в регистрах. В большинстве случаев летучий не является необходимым.
Однажды я провел некоторое тестирование в MSVC, проверив выходные данные ассемблера на различные ситуации, и все, что нужно для предотвращения кэширования переменной, - это заставить другую функцию записать эту же переменную или получить ее адрес. Глобальные переменные никогда не оптимизировались, если не была включена "оптимизация всей программы" (поэтому компилятор может быть уверен, что переменная не была изменена в других единицах перевода).
Чтобы проконтролировать ответ Майка, это полезно в таких случаях (глобальные переменные используются для избежания сложности в этом примере):
static volatile bool thread_running = true;
static void thread_body() {
while (thread_running) {
// ...
}
}
static void abort_thread() {
thread_running = false;
}
В зависимости от того, насколько сложным thread_body
компилятор может выбрать кэширование значения thread_running
в регистре, когда поток начинает работать, это означает, что он никогда не заметит, если значение изменится на false. volatile
заставляет компилятор выдавать код, который будет проверять фактическое thread_running
переменная в каждом цикле.
Я бы предложил значительно более строгое, но очень полезное правило: если вы не понимаете, что именно volatile
делает, не используйте его. Вместо этого используйте lock
, Если вы не понимаете, что именно lock
делает и как его использовать, не используйте многопоточность.
Аналогичным образом, стандарт C++ не определяет, как должен работать volatile, вы должны посмотреть, что конкретный компилятор делает для конкретной платформы. Кроме того, volatile немного неуловимо, и то, что он делает, зависит от компилятора и модели памяти целевой платформы. Замки более интуитивно понятны в использовании.
Прежде чем воспринимать статью, на которую вы ссылаетесь, слишком серьезно, вы можете прочитать другую. В более поздней публикации на Usenet Андрей позже признал, что части оригинальной статьи были просто неправильными, и указал на эту статью как на более реалистичное представление о вещах (хотя обратите внимание, что ссылка, которую он дает на нее, кажется, устарела).