Два потока выполняют операции над двумя атомиками. Гарантирует ли memory_order_seq_cst, что утверждение не сработает?
#include <atomic>
#include <cassert>
#include <thread>
std::atomic<int> x{0}, y{0};
int r1 = 0, r2 = 0;
int main() {
std::thread t1([] {
x.store(1, std::memory_order_seq_cst);
r1 = y.load(std::memory_order_seq_cst);
});
std::thread t2([] {
y.store(1, std::memory_order_seq_cst);
r2 = x.load(std::memory_order_seq_cst);
});
t1.join(); t2.join();
assert(!(r1 == 0 && r2 == 0));
}
Да, при корректном выполнении программы утверждение не сработает: оба потока не могут одновременно прочитать 0. Операции с memory_order_seq_cst подчиняются единому порядку, согласованному с порядком операций внутри каждого потока.
Один из результатов может быть 0, а оба результата могут быть 1. Запрещена только комбинация r1 == 0 && r2 == 0.
Модель памяти C++ появилась для переносимого описания взаимодействия потоков на архитектурах с разными правилами переупорядочивания операций. Без формальной модели исходный код мог выглядеть одинаково, но иметь разные допустимые результаты на разных процессорах и компиляторах.
Последовательно согласованный порядок (seq_cst) предоставляет наиболее простой для рассуждения режим атомиков: все последовательные атомарные операции можно представить как находящиеся в одном общем порядке, сохраняющем порядок каждого отдельного потока.
Каждый поток сначала записывает 1 в свой атомик, затем читает другой атомик. На слабой модели памяти интуитивно ожидаемая последовательность может нарушаться: оба чтения потенциально могли бы увидеть исходное значение 0.
Такой результат означал бы, что каждый поток увидел чтение раньше записи другого потока, хотя внутри каждого потока запись должна предшествовать собственному чтению. Нужно определить, допускает ли это выбранный порядок памяти.
Обозначим операции так: Sx — запись x, Ly — чтение y, Sy — запись y, Lx — чтение x. Порядок внутри потоков требует Sx < Ly и Sy < Lx.
Если r1 == 0, чтение Ly должно находиться в общем порядке до Sy: Ly < Sy. Если r2 == 0, аналогично должно выполняться Lx < Sx. Получается цикл ограничений: Sx < Ly < Sy < Lx < Sx, что невозможно в едином ацикличном порядке.
Поэтому оба чтения нулей запрещены. При этом seq_cst не требует, чтобы оба чтения увидели 1: общий порядок может поставить одно чтение до записи другого атомика, но не может поставить оба чтения до соответствующих записей.
Seq_cst упрощает доказательство корректности, но может ограничивать оптимизации сильнее, чем acquire/release или relaxed. Кроме того, его гарантии относятся к атомарным операциям; обычные данные нельзя безопасно читать и писать из разных потоков без подходящей синхронизации.
Важно, что присваивания r1 и r2 не создают гонку в данном примере: каждый из них записывается только своим потоком, а чтение происходит после join(). join() завершает выполнение потока до продолжения вызывающего потока.
В lock-free очереди разработчик использовал два атомика как флаги состояний и заметил редкий результат, соответствующий чтению старых значений обоими потребителями. Сначала рассматривались relaxed — быстрый вариант, но он не давал нужного глобального порядка; и отдельная блокировка — проще для доказательства, но с дополнительными затратами и возможностью блокировок.
Для короткого протокола с небольшим числом операций выбрали seq_cst: он напрямую запрещал наблюдавшийся результат и упростил аудит. После измерений выяснилось, что этот участок не был узким местом; если бы измерения показали существенную стоимость, следующим шагом была бы замена на более слабые порядки с формальным доказательством необходимых отношений, а не механическое ослабление.
seq_cst любой результат, содержащий ноль?Нет. Например, допустим результат r1 == 0, r2 == 1: чтение y может произойти до записи y, тогда как чтение x — после записи x. Запрещена именно комбинация двух нулей, потому что только она образует цикл в едином порядке.
x и y, если вместо них читается обычное поле объекта?Нет. Атомарность отдельных флагов не делает автоматически безопасными связанные с ними обычные данные. Для публикации обычного объекта нужны корректные отношения happens-before, обычно через release/acquire, mutex или другой механизм синхронизации; иначе чтение и запись обычного поля могут образовать гонку данных.
seq_cst на relaxed, сохранив запрет результата (0, 0)?Нет, такой запрет из relaxed не следует. relaxed гарантирует атомарность и согласованность модификаций каждого отдельного атомика, но не создаёт общего порядка между операциями над x и y. На подходящей архитектуре и при допустимой компилятору реализации оба чтения могут увидеть исходные значения.