В многопоточном Rust поток записывает данные, затем выставляет атомарный флаг готовности, а другой поток читает флаг и данные. Почему для публикации данных пары операций с порядком Relaxed недостаточно?
Relaxed гарантирует атомарность самой операции, но не устанавливает порядок видимости обычных предшествующих записей для другого потока. Для публикации данных обычно используют запись флага с порядком Release и чтение флага с Acquire: если чтение наблюдает значение, опубликованное Release-записью, все предшествующие записи первого потока становятся видимыми второму.
Современные процессоры и оптимизирующие компиляторы могут менять порядок независимых операций, если это не нарушает поведение одного потока. Поэтому атомарность операции и порядок взаимодействия между потоками — разные свойства.
Модель памяти Rust позволяет явно выбрать требуемую силу гарантии. Это помогает не платить за более строгую синхронизацию там, где достаточно локальной атомарности, но требует явно описывать отношения между публикацией и чтением данных.
Пусть производитель сначала записывает результат, а затем устанавливает флаг готовности. Потребитель видит установленный флаг и ожидает, что результат уже корректно опубликован.
При Relaxed компилятор и процессор не обязаны создать отношение happens-before между записью результата и последующим чтением результата. Потребитель может наблюдать новое значение флага, но не получить гарантии видимости связанных с ним данных.
Для неатомарных данных неправильная синхронизация особенно опасна: если один поток одновременно изменяет данные, а другой читает их без установленного отношения happens-before, программа может иметь гонку данных и не соответствовать требованиям модели памяти Rust.
Производитель выполняет запись данных, затем store флага с порядком Release. Потребитель выполняет load флага с порядком Acquire и продолжает работу только после того, как увидел опубликованное значение.
Release запрещает переносить публикацию флага перед предшествующими операциями в значимой для модели памяти части программы. Acquire не позволяет последующим операциям потребителя быть выполненными до успешного наблюдения публикации. Вместе эти операции образуют межпоточное отношение синхронизации.
Минимальный пример с двумя атомарными значениями:
VALUE может использовать Relaxed, потому что порядок публикации обеспечивает флаг READY. Вызов Acquire должен наблюдать значение, опубликованное соответствующей Release-операцией; простое наличие двух атомарных операций само по себе такой гарантии не даёт.
SeqCst дополнительно устанавливает единый порядок всех последовательных атомарных операций между потоками. Это проще для рассуждений в сложных алгоритмах, но обычно сильнее и потенциально дороже, чем необходимая пара Release/Acquire. Relaxed подходит для счётчиков и других случаев, где нужна только атомарность и не требуется публикация связанных данных.
Атомарный флаг не делает произвольную структуру автоматически потокобезопасной. Нужно также обеспечить корректное владение объектом, отсутствие одновременной несинхронизированной модификации и соблюдение правил Send и Sync.
В системе один поток формирует снимок состояния, а другой должен начать обработку только после его полной подготовки. Рассматривались три варианта: защищать снимок через Mutex, использовать SeqCst для всех атомарных операций или применить отдельный флаг Release/Acquire.
Mutex проще проверить, но добавляет блокировки и не нужен, если снимок после публикации неизменяем. SeqCst даёт более сильную гарантию, однако не требуется для простой схемы публикации. Выбран флаг Release/Acquire: производитель завершает подготовку и публикует готовность, а потребитель после Acquire безопасно читает неизменяемый снимок.
Такое решение уменьшает область синхронизации, но требует документировать протокол: флаг нельзя выставлять раньше завершения подготовки, а опубликованные данные нельзя изменять без отдельного механизма синхронизации.
Нет. Release на стороне производителя публикует предшествующие записи, но потребитель должен выполнить Acquire, чтобы принять эту публикацию и получить соответствующую гарантию видимости. Relaxed-чтение может увидеть новое значение флага без установления нужного отношения happens-before.
Если Acquire действительно наблюдает значение, опубликованное Release-записью, то все операции производителя до Release происходят раньше операций потребителя после Acquire в модели памяти. Поэтому последующий Relaxed-загрузчик атомарного поля данных уже выполняется в установленном порядке публикации.
Это не означает, что любое Relaxed-чтение в любой момент безопасно. Гарантия появляется только после успешной синхронизации через флаг и при соблюдении протокола: данные были записаны до Release и больше не изменяются конкурентно без дополнительной синхронизации.
SeqCst нужен не просто потому, что программа многопоточная. Его выбирают, когда алгоритму требуется единый глобальный порядок последовательных атомарных операций, например для рассуждений о нескольких флагах и сложных взаимных наблюдениях.
Для однонаправленной публикации одного набора данных Release/Acquire обычно достаточно. Если алгоритм можно корректно описать через локальные отношения публикации и потребления, SeqCst будет избыточным; если же доказательство опирается на общий порядок событий, более сильный порядок может сделать алгоритм понятнее и безопаснее.