Как изменения (чтение/запись) в std::атомарные переменные распространяются по потокам


Недавно я задал этот вопрос Нужно ли мне использовать барьеры памяти для защиты общего ресурса

На этот вопрос я получил очень интересный ответ, который использует эту гипотезу:

Changes to std::atomic variables are guaranteed to propagate across threads.

Почему это так? Как это делается? Как это поведение вписывается в протокол MESI ?

1 2

1 ответ:

Они на самом деле не должны распространяться, модель когерентности кэша (MESI или что-то более продвинутое) дает вам гарантию того, что память ведет себя когерентно, почти как если бы она была плоской и никаких кэшированных копий не существовало. Последовательная согласованность добавляет к этому гарантию одинакового порядка наблюдения всеми агентами в системе (обратите внимание - большинство процессоров не обеспечивают последовательную согласованность только через HW).

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

Теперь вы спрашиваете, почему важна атомная энергия? По двум причинам. Во - первых-все вышесказанное применимо только в том случае, если переменная находится в памяти, а не в регистре. Это решение компилятора, поэтому правильный тип говорит ему сделать это. Другие парадигмы (например, потоки open-MP или POSIX) имеют другие способы сообщить компилятору, что переменная должна быть разделена через память. Второе-современные ядра выполняют операции мы не хотим, чтобы какая-либо другая операция передавала эту запись и предоставляла устаревшие данные. std:: atomic говорит компилятору принудительно применять самый строгий порядок памяти (с помощью явного ограждения или блокировки - проверьте сгенерированный ассемблерный код), что означает, что все операции памяти из всех потоков будут иметь одинаковый глобальный порядок. Если вы этого не сделали, могут произойти странные вещи, такие как ядро A и ядро B, несогласные по порядку 2 записи в одно и то же место (имеется в виду что они могут видеть в нем разные конечные ценности).

Последним, конечно, является фактическая атомарность - если ваш тип данных не имеет гарантированной атомарности или он не выровнен должным образом - это также решит эту проблему для вас (в противном случае проблема когерентности усиливается-подумайте о том, что какой-то поток пытается изменить значение, разделенное между 2 строками кэша, и разные ядра видят частичные значения)