Модуль 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 reach
  • Condvar: Условная переменная для блокировки и пробуждения потоков
  • 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] - Псевдоним типа для результата неблокирующего метода блокировки.