Как мьютекс обеспечивает согласованное значение переменной по всем ядрам?
Если у меня есть один int, который я хочу записать из одного потока и читать из другого, мне нужно использовать std::atomic
, чтобы убедиться, что его значение согласовано по всем ядрам, независимо от того, являются ли инструкции, которые чтение и запись на него концептуально атомарны. Если я этого не сделаю, может быть, что ядро чтения имеет старое значение в своем кеше и не увидит новое значение. Это имеет смысл для меня.
Если у меня есть сложный тип данных, который нельзя прочитать/записать в атомарном режиме, мне нужно защитить доступ к нему с помощью некоторого примитива синхронизации, например std::mutex
. Это предотвратит проникновение объекта (или чтение из него) в несогласованное состояние. Это имеет смысл для меня.
Что для меня не имеет смысла, так это то, как мьютексы помогают с проблемой кэширования, которую атомизация решает. Кажется, что они существуют исключительно для предотвращения параллельного доступа к некоторому ресурсу, но не для распространения каких-либо значений, содержащихся в этом ресурсе, в кэши других ядер. Есть ли какая-то часть их семантики, которую я пропустил, что касается этого?
Ответы
Ответ 1
Правильный ответ на это - волшебные пикси - например. Это просто работает. Реализация std:: atomic для каждой платформы должна быть правильной.
Правильная вещь - это комбинация из трех частей.
Во-первых, компилятор должен знать, что он не может перемещать инструкции через границы [на самом деле он может в некоторых случаях, но предположить, что это не так).
Во-вторых, необходимо знать подсистему кэша/памяти - обычно это делается с использованием барьеров памяти, хотя x86/x64 обычно имеют такую сильную память, что это не обязательно в подавляющем большинстве случаев (что является большим позором так как это хорошо для неправильного кода, чтобы на самом деле пойти не так).
Наконец, CPU должен знать, что он не может изменить порядок инструкций. Современные процессоры являются массово агрессивными при переупорядочивании и гарантируют, что в однопоточном корпусе это незаметно. Им может потребоваться больше намеков на то, что это не может произойти в определенных местах.
Для большинства процессоров часть 2 и 3 сводится к одному и тому же - барьер памяти подразумевает оба. Часть 1 полностью встроена в компилятор, и разработчикам компилятора удается доработать.
См. статью Herb Sutters "Atomic Weapons" для более интересной информации.
Ответ 2
Консистенция между ядрами обеспечивается барьерами памяти (что также предотвращает переупорядочение команд). Когда вы используете std::atomic
, вы не только получаете доступ к данным атомарно, но и компилятор (и библиотека) также вставляете соответствующие барьеры памяти.
Мьютексы работают одинаково: реализации мьютексов (например, pthreads или WinAPI или что не так) внутренне также вставляют барьеры памяти.
Ответ 3
Большинство современных многоядерных процессоров (включая x86 и x64) cache coherent. Если два ядра хранят одно и то же место в кэше, а одно из них обновляет значение, это изменение автоматически распространяется на кэши других ядер. Он неэффективен (запись в одну и ту же линию кэша одновременно с двух ядер очень медленная), но без согласования кеша было бы очень сложно написать многопоточное программное обеспечение.
И, как сказал сям, также требуются барьеры памяти. Они не позволяют компилятору или процессору переупорядочивать обращения к памяти, а также заставляют записывать в память (или, по крайней мере, в кеш), когда, например, переменная хранится в регистре из-за оплотов компилятора.