Перейти к основному содержимому
  1. Rust/

Атомарные операции и Memory Ordering в Rust: от Ordering::Relaxed до Acquire/Release барьеров

1001 слово·5 минут· loading · loading · · ·Rust-middle Черновик
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
40 Атомарные операции и Memory Ordering в Rust: от Ordering::Relaxed до Acquire/Release барьеров (текущая)
Не прочитана

Введение
#

При написании многопоточных систем обычные блокировки (Mutex и RwLock) обеспечивают полную безопасность за счет приостановки потоков операционной системой. Однако контекстные переключения процессора и системные вызовы ядер ОС несут ощутимые накладные расходы.

В высоконагруженных lock-free алгоритмах, высокочастотных сетевых сервисах и ядрах СУБД для изменения данных используют Атомарные операции (Atomics).

Атомарная операция выполняется процессором как одна неделимая инструкция, которую не может прервать или увидеть в промежуточном состоянии ни один другой поток.


1. Атомарные типы (std::sync::atomic)
#

Модуль std::sync::atomic предоставляют скалярные типы с поддержкой аппаратной атомарности: AtomicBool, AtomicUsize, AtomicI32, AtomicPtr<T>.

Ключевая особенность атомарных типов в Rust — Внутренняя мутабельность (Interior Mutability) на уровне железа. Вы можете безопасно изменять атомарные данные по обычным иммутабельным ссылкам &self из нескольких потоков одновременно.

Базовые методы атомиков:

  • load / store: атомарное чтение и запись.
  • fetch_add / fetch_sub: атомарный инкремент/декремент (возвращают предыдущее значение).
  • swap: атомарный обмен значениями.
  • compare_exchange: атомарная операция Compare-And-Swap (CAS).

Ниже представлен пошаговый пример работы атомарного счетчика и флага останова:

Атомарные типы и счетчик без мьютекса
Шаг 1/2
 1use std::sync::atomic::{AtomicUsize, Ordering};
 2use std::sync::Arc;
 3use std::thread;
 4
 5// Шаг 1: Атомарный счетчик AtomicUsize без мьютекса
 6fn main() {
 7    // AtomicUsize изменяется по разделяемой иммутабельной ссылке (&self) без блокировки Mutex!
 8    let counter = Arc::new(AtomicUsize::new(0));
 9    let mut handles = vec![];
10
11    for _ in 0..5 {
12        let counter_clone = Arc::clone(&counter);
13        let handle = thread::spawn(move || {
14            for _ in 0..100 {
15                // fetch_add атомарно прибавляет 1 и возвращает предыдущее значение
16                counter_clone.fetch_add(1, Ordering::Relaxed);
17            }
18        });
19        handles.push(handle);
20    }
21
22    for handle in handles {
23        handle.join().unwrap();
24    }
25
26    println!("Итоговое значение атомарного счетчика: {}", counter.load(Ordering::Relaxed));
27}
28

1. Атомарные скалярные примитивы

  • Типы AtomicUsize, AtomicBool, AtomicI32 позволяют изменять значения по иммутабельной ссылке &self (Interior Mutability на уровне железа).
  • Операции fetch_add, fetch_sub, store и load выполняются одной непрерывной процессорной инструкцией.

2. Проблема переупорядочивания инструкций (Instruction Reordering)
#

Современные процессоры и оптимизирующие компиляторы стремительно переупорядочивают инструкции выполнения для достижения максимального параллелизма на уровне инструкций (ILP).

Если поток 1 сначала пишет data = 42, а затем устанавливает flag = true, проц или компилятор могут легко поменять эти инструкции местами, если между ними нет зависимости по данным!

В итоге поток 2 может увидеть flag == true, но прочитать из data мусорное или старое значение.

Для решения этой проблемы в вызовы атомиков передается параметр Ordering — барьер памяти (Memory Barrier).


3. Модели упорядочивания памяти (Memory Ordering)
#

В Rust предусмотрено 5 уровней порядка памяти:

  1. Ordering::Relaxed:
    • Гарантирует только атомарность самой операции над данной ячейкой памяти.
    • Не создает никаких барьеров вокруг: соседние операции чтения и записи могут свободно переупорядочиваться.
    • Подходит для изолированных счетчиков метрик.
  2. Ordering::Release (для записи):
    • Создает барьер публикации: компилятор и процессор не имеют права переносить операции записи, расположенные до Release, на место после него.
  3. Ordering::Acquire (для чтения):
    • Создает барьер чтения: операции чтения, расположенные после Acquire, *не могут быть выполнены до него.
    • Пара Release + Acquire образует синхронизационную связь (Happens-Before relation) между потоками без единой системной блокировки!
  4. Ordering::SeqCst (Sequentially Consistent):
    • Гарантирует строгий глобальный единый порядок всех операций во всей программе.
    • Самый безопасный режим (используется по умолчанию во многих языках), но несет наибольшие накладные расходы на шине процессора.
Совет

В 90% практических случаев применения lock-free флагов используется пара Ordering::Release при установке флага и Ordering::Acquire при его проверке.

Запустите примеры барьеров памяти Acquire/Release и операции compare_exchange:

Барьеры памяти Acquire/Release и CAS
Шаг 1/2
 1use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
 2use std::sync::Arc;
 3use std::thread;
 4
 5// Шаг 1: Синхронизация данных через Acquire/Release барьер памяти
 6fn main() {
 7    let data = Arc::new(AtomicU8::new(0));
 8    let ready_flag = Arc::new(AtomicBool::new(false));
 9
10    let data_writer = Arc::clone(&data);
11    let flag_writer = Arc::clone(&ready_flag);
12
13    // Поток-Писатель публикует данные
14    thread::spawn(move || {
15        data_writer.store(42, Ordering::Relaxed);
16        // Ordering::Release гарантирует, что ВСЕ предыдущие записи видны после этого момента
17        flag_writer.store(true, Ordering::Release);
18    });
19
20    let data_reader = Arc::clone(&data);
21    let flag_reader = Arc::clone(&ready_flag);
22
23    // Поток-Читатель ожидает флаг
24    let handle = thread::spawn(move || {
25        // Ordering::Acquire создает барьер чтения
26        while !flag_reader.load(Ordering::Acquire) {
27            thread::yield_now();
28        }
29        // Защищено барьером: data_reader гарантированно видит значение 42!
30        println!("Прочитанные данные через Acquire/Release: {}", data_reader.load(Ordering::Relaxed));
31    });
32
33    handle.join().unwrap();
34}
35

1. Барьеры памяти Acquire и Release

  • Ordering::Release создаёт барьер при записи: гарантирует, что все операции с памятью ДО записи не будут переупорядочены ПОСЛЕ неё.
  • Ordering::Acquire создаёт барьер при чтении: гарантирует, что операции ПОСЛЕ чтения не будут выполнены ДО него.
  • Пара Release + Acquire синхронизирует состояние между потоками без блокировок.

Проверь свои знания!
#

Пройдите короткий тест по атомикам и Memory Ordering:

Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/58
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
40 Атомарные операции и Memory Ordering в Rust: от Ordering::Relaxed до Acquire/Release барьеров (текущая)
Не прочитана

Связанные статьи