Модуль sync
Полезные примитивы синхронизации.
Необходимость синхронизации
Концептуально, программа на Rust - это серия операций, которые будут выполнены на компьютере. Временная шкала событий, происходящих в программе, согласуется с порядком операций в коде.
Рассмотрим следующий код, работающий с некоторыми глобальными статическими переменными:
// FIXME(static_mut_refs): Не разрешать линт `static_mut_refs` #![allow(static_mut_refs)] static mut A: u32 = 0; static mut B: u32 = 0; static mut C: u32 = 0; fn main() { unsafe { A = 3; B = 4; A = A + B; C = B; println!("{A} {B} {C}"); C = A; } }
Кажется, что некоторые переменные, хранящиеся в памяти, изменяются, выполняется сложение, результат сохраняется в A, а переменная C изменяется дважды.
Когда задействован только один поток, результаты ожидаемы: выводится строка 7 4 4.
Что касается того, что происходит за кулисами, при включённых оптимизациях итоговый сгенерированный машинный код может сильно отличаться от кода:
- Первое сохранение в C может быть перемещено перед сохранением в A или B, как если бы мы написали
C = 4; A = 3; B = 4. - Присваивание A + B в A может быть удалено, поскольку сумма может храниться во временном местоположении до вывода, а глобальная переменная никогда не обновляется.
- Конечный результат может быть определён просто просмотром кода во время компиляции, поэтому свёртка констант может превратить весь блок в простой
println!("7 4 4").
Компилятору разрешено выполнять любую комбинацию этих оптимизаций, пока итоговый оптимизированный код при выполнении даёт те же результаты, что и без оптимизаций.
Из-за параллелизма, присущего современным компьютерам, предположения о порядке выполнения программы часто неверны. Доступ к глобальным переменным может приводить к недетерминированным результатам, даже если оптимизации компилятора отключены, и всё ещё возможно внесение ошибок синхронизации.
Заметьте, что благодаря гарантиям безопасности Rust, доступ к глобальным (static) переменным требует небезопасного кода, при условии, что мы не используем никакие примитивы синхронизации из этого модуля.
Неупорядоченное выполнение
Инструкции могут выполняться в порядке, отличном от определённого нами, по разным причинам:
- Переупорядочивание инструкций компилятором: Если компилятор может выдать инструкцию в более ранней точке, он попытается это сделать. Например, он может поднять загрузки памяти в начало блока кода, чтобы CPU мог начать предварительную выборку значений из памяти.
- В однопоточных сценариях это может вызывать проблемы при написании обработчиков сигналов или определённых видов низкоуровневого кода. Используйте барьеры компилятора для предотвращения этого переупорядочивания.
- Выполнение инструкций одним процессором вне порядка: Современные CPU способны к суперскалярному выполнению, т.е. несколько инструкций могут выполняться одновременно, даже though машинный код описывает последовательный процесс.
- Этот вид переупорядочивания прозрачно обрабатывается CPU.
- Многопроцессорная система, выполняющая несколько аппаратных потоков одновременно: В многопоточных сценариях вы можете использовать два вида примитивов для работы с синхронизацией:
- Барьеры памяти для обеспечения того, что обращения к памяти становятся видимыми другим CPU в правильном порядке.
- Атомарные операции для обеспечения того, что одновременный доступ к одному и тому же местоположению памяти не приводит к неопределённому поведению.
Высокоуровневые объекты синхронизации
Большинство низкоуровневых примитивов синхронизации довольно подвержены ошибкам и неудобны в использовании, поэтому стандартная библиотека также предоставляет некоторые высокоуровневые объекты синхронизации.
Эти абстракции могут быть построены из низкоуровневых примитивов. Для эффективности, объекты синхронизации в стандартной библиотеке обычно реализуются с помощью ядра операционной системы, которое может перепланировать потоки, пока они заблокированы на захвате блокировки.
Ниже приведён обзор доступных объектов синхронизации:
- Arc: Атомарно подсчитываемая ссылка (Atomically Reference-Counted pointer), которая может использоваться в многопоточных средах для продления времени жизни некоторых данных, пока все потоки не закончат их использование.
- Barrier: Обеспечивает ожидание несколькими потоками друг друга для достижения точки в программе перед продолжением выполнения все вместе.
- Condvar: Условная переменная (Condition Variable), предоставляющая возможность блокировать поток в ожидании наступления события.
- mpsc: Очереди с несколькими производителями и одним потребителем (Multi-producer, single-consumer), используемые для общения на основе сообщений. Могут предоставлять лёгкий механизм межпоточной синхронизации ценой дополнительной памяти.
- mpmc: Очереди с несколькими производителями и несколькими потребителями (Multi-producer, multi-consumer), используемые для общения на основе сообщений. Могут предоставлять лёгкий механизм межпоточной синхронизации ценой дополнительной памяти.
- Mutex: Механизм взаимного исключения (Mutual Exclusion), который обеспечивает, что не более одного потока в данный момент времени может обращаться к некоторым данным.
- Once: Используется для потокобезопасной однократной глобальной инициализации. В основном полезен для реализации других типов, таких как OnceLock.
- OnceLock: Используется для потокобезопасной однократной инициализации переменной с потенциально разными инициализаторами в зависимости от вызывающей стороны.
- LazyLock: Используется для потокобезопасной однократной инициализации переменной с использованием одной нульарной функции инициализации, предоставленной при создании.
- RwLock: Предоставляет механизм взаимного исключения, который позволяет нескольким читателям одновременно, while позволяя только одному писателю за раз. В некоторых случаях это может быть эффективнее мьютекса.
Модули
- atomic
- Атомарные типы
- mpsc
- Примитивы общения через очередь FIFO с несколькими производителями и одним потребителем
- mpmc Experimental
- Примитивы общения через очередь FIFO с несколькими производителями и несколькими потребителями
- nonpoison Experimental
- Синхронные блокировки без отравления
- poison Experimental
- Объекты синхронизации, использующие отравление
Многопоточность с поддержкой владения
Традиционно одновременный доступ к данным из нескольких потоков в Rust решается с помощью примитивов синхронизации, которые несут ответственность за синхронизацию доступа к данным, в отличие от передачи данных между потоками (как в каналах).
Модуль std::sync содержит следующие основные примитивы:
Arc: Потокобезопасный счетчик ссылок с атомарными операциямиMutex: Механизм взаимного исключения, обеспечивающий эксклюзивный доступ к даннымRwLock: Примитив "чтение-запись", позволяющий множественное чтение или эксклюзивную записьBarrier: Обеспечивает точку синхронизации, где multiple threads will wait for all to reachCondvar: Условная переменная для блокировки и пробуждения потоковOnce: Однократная инициализация для глобальных значенийmpsc: Многопоточные каналы "multiple-producer, single-consumer"atomic: Атомарные типы и операции
Многие примитивы синхронизации построены на основе атомарных операций, которые также доступны в этом модуле через подмодуль atomic. Атомарные типы представляют собой строительные блоки для примитивов синхронизации без блокировок и являются основой для всего многопоточного программирования в Rust.
Пример
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; use std::thread; let data = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let data = Arc::clone(&data); let handle = thread::spawn(move || { let mut data = data.lock().unwrap(); *data += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } assert_eq!(*data.lock().unwrap(), 10); }
Структуры
Arc- Потокобезопасный указатель с подсчётом ссылок. «Arc» означает «Atomically Reference Counted» (Атомарно Подсчитываемая Ссылка).Barrier- Барьер позволяет нескольким потокам синхронизировать начало некоторого вычисления.BarrierWaitResult- BarrierWaitResult возвращается методом Barrier::wait(), когда все потоки в Barrier встретились.Condvar- Условная переменная.LazyLock- Значение, которое инициализируется при первом доступе.Mutex- Примитив взаимного исключения, полезный для защиты общих данных.MutexGuard- RAII-реализация «scoped lock» (блокировки с ограниченной областью видимости) мьютекса. Когда эта структура удаляется (выходит из области видимости), блокировка будет снята.Once- Низкоуровневый примитив синхронизации для однократного глобального выполнения.OnceLock- Примитив синхронизации, который номинально может быть записан только один раз.OnceState- Состояние, передаваемое в параметр замыкания Once::call_once_force(). Состояние может использоваться для запроса статуса отравления Once.PoisonError- Тип ошибки, который может возвращаться при захвате блокировки.RwLock- Читательско-писательская блокировка.RwLockReadGuard- RAII-структура, используемая для освобождения общего доступа на чтение блокировки при удалении.RwLockWriteGuard- RAII-структура, используемая для освобождения эксклюзивного доступа на запись блокировки при удалении.WaitTimeoutResult- Тип, указывающий, вернулась ли timed wait (ожидание с таймаутом) на условной переменной из-за таймаута или нет.Weak- Weak - это версия Arc, которая содержит невладеющую ссылку на управляемое выделение памяти.- [
Exclusive] - Experimental Exclusive предоставляет только изменяемый доступ, также называемый эксклюзивным доступом к базовому значению. Он не предоставляет неизменяемый или общий доступ к базовому значению. MappedMutexGuard- Experimental RAII-страж мьютекса, возвращаемый MutexGuard::map, который может указывать на подполе защищенных данных. Когда эта структура удаляется (выходит из области видимости), блокировка будет снята.MappedRwLockReadGuard- Experimental RAII-структура, используемая для освобождения общего доступа на чтение блокировки при удалении, которая может указывать на подполе защищенных данных.MappedRwLockWriteGuard- Experimental RAII-структура, используемая для освобождения эксклюзивного доступа на запись блокировки при удалении, которая может указывать на подполе защищенных данных.- [
ReentrantLock] - Experimental Реентерабельная (повторно входимая) блокировка взаимного исключения. - [
ReentrantLockGuard] - Experimental RAII-реализация «scoped lock» (блокировки с ограниченной областью видимости) реентерабельной блокировки. Когда эта структура удаляется (выходит из области видимости), блокировка будет снята. - [
UniqueArc] - Experimental Уникально владеющий Arc.
Перечисления
TryLockError- Перечисление возможных ошибок, связанных с TryLockResult, которые могут возникнуть при попытке захвата блокировки, из метода try_lock на Mutex или методов try_read и try_write на RwLock.
Константы
- [
ONCE_INIT] - Deprecated Инициализирующее значение для статических значений Once.
Псевдонимы типов
-
[
LockResult] - Псевдоним типа для результата метода блокировки, который может быть отравлен. -
[
TryLockResult] - Псевдоним типа для результата неблокирующего метода блокировки.