При написании многопоточных систем обычные блокировки (Mutex и RwLock) обеспечивают полную безопасность за счет приостановки потоков операционной системой. Однако контекстные переключения процессора и системные вызовы ядер ОС несут ощутимые накладные расходы.
В высоконагруженных lock-free алгоритмах, высокочастотных сетевых сервисах и ядрах СУБД для изменения данных используют Атомарные операции (Atomics).
Атомарная операция выполняется процессором как одна неделимая инструкция, которую не может прервать или увидеть в промежуточном состоянии ни один другой поток.
Модуль std::sync::atomic предоставляют скалярные типы с поддержкой аппаратной атомарности: AtomicBool, AtomicUsize, AtomicI32, AtomicPtr<T>.
Ключевая особенность атомарных типов в Rust — Внутренняя мутабельность (Interior Mutability) на уровне железа. Вы можете безопасно изменять атомарные данные по обычным иммутабельным ссылкам &self из нескольких потоков одновременно.
compare_exchange: атомарная операция Compare-And-Swap (CAS).
Ниже представлен пошаговый пример работы атомарного счетчика и флага останова:
Атомарные типы и счетчик без мьютекса
Шаг 1/2
1usestd::sync::atomic::{AtomicUsize,Ordering}; 2usestd::sync::Arc; 3usestd::thread; 4 5// Шаг 1: Атомарный счетчик AtomicUsize без мьютекса
6fnmain(){ 7// AtomicUsize изменяется по разделяемой иммутабельной ссылке (&self) без блокировки Mutex!
8letcounter=Arc::new(AtomicUsize::new(0)); 9letmuthandles=vec![];1011for_in0..5{12letcounter_clone=Arc::clone(&counter);13lethandle=thread::spawn(move||{14for_in0..100{15// fetch_add атомарно прибавляет 1 и возвращает предыдущее значение
16counter_clone.fetch_add(1,Ordering::Relaxed);17}18});19handles.push(handle);20}2122forhandleinhandles{23handle.join().unwrap();24}2526println!("Итоговое значение атомарного счетчика: {}",counter.load(Ordering::Relaxed));27}28
1. Атомарные скалярные примитивы
Типы AtomicUsize, AtomicBool, AtomicI32 позволяют изменять значения по иммутабельной ссылке &self (Interior Mutability на уровне железа).
Операции fetch_add, fetch_sub, store и load выполняются одной непрерывной процессорной инструкцией.
Результат выполнения:
1usestd::sync::atomic::{AtomicBool,Ordering}; 2usestd::sync::Arc; 3usestd::thread; 4usestd::time::Duration; 5 6// Шаг 2: Флаг останова фонового потока на основе AtomicBool
7fnmain(){ 8letrunning_flag=Arc::new(AtomicBool::new(true)); 9letflag_clone=Arc::clone(&running_flag);1011lethandle=thread::spawn(move||{12letmutiterations=0;13whileflag_clone.load(Ordering::Relaxed){14iterations+=1;15thread::sleep(Duration::from_millis(10));16}17println!("Фоновый поток завершен после {} итераций",iterations);18});1920thread::sleep(Duration::from_millis(50));21// Сигнализируем фоновому потоку о необходимости останова
22running_flag.store(false,Ordering::Relaxed);2324handle.join().unwrap();25}26
2. Атомарные флаги остановки (Lock-Free Signals)
Использование AtomicBool для передачи сигналов остановки между потоками не требует тяжелой блокировки мьютекса.
Результат выполнения:
2. Проблема переупорядочивания инструкций (Instruction Reordering)
#
Современные процессоры и оптимизирующие компиляторы стремительно переупорядочивают инструкции выполнения для достижения максимального параллелизма на уровне инструкций (ILP).
Если поток 1 сначала пишет data = 42, а затем устанавливает flag = true, проц или компилятор могут легко поменять эти инструкции местами, если между ними нет зависимости по данным!
В итоге поток 2 может увидеть flag == true, но прочитать из data мусорное или старое значение.
Для решения этой проблемы в вызовы атомиков передается параметр Ordering — барьер памяти (Memory Barrier).
3. Модели упорядочивания памяти (Memory Ordering)
#
В Rust предусмотрено 5 уровней порядка памяти:
Ordering::Relaxed:
Гарантирует только атомарность самой операции над данной ячейкой памяти.
Не создает никаких барьеров вокруг: соседние операции чтения и записи могут свободно переупорядочиваться.
Подходит для изолированных счетчиков метрик.
Ordering::Release (для записи):
Создает барьер публикации: компилятор и процессор не имеют права переносить операции записи, расположенные доRelease, на место после него.
Ordering::Acquire (для чтения):
Создает барьер чтения: операции чтения, расположенные послеAcquire, *не могут быть выполнены до него.
Пара Release + Acquire образует синхронизационную связь (Happens-Before relation) между потоками без единой системной блокировки!
Ordering::SeqCst (Sequentially Consistent):
Гарантирует строгий глобальный единый порядок всех операций во всей программе.
Самый безопасный режим (используется по умолчанию во многих языках), но несет наибольшие накладные расходы на шине процессора.
Совет
В 90% практических случаев применения lock-free флагов используется пара Ordering::Release при установке флага и Ordering::Acquire при его проверке.
Запустите примеры барьеров памяти Acquire/Release и операции compare_exchange:
Барьеры памяти Acquire/Release и CAS
Шаг 1/2
1usestd::sync::atomic::{AtomicBool,AtomicU8,Ordering}; 2usestd::sync::Arc; 3usestd::thread; 4 5// Шаг 1: Синхронизация данных через Acquire/Release барьер памяти
6fnmain(){ 7letdata=Arc::new(AtomicU8::new(0)); 8letready_flag=Arc::new(AtomicBool::new(false)); 910letdata_writer=Arc::clone(&data);11letflag_writer=Arc::clone(&ready_flag);1213// Поток-Писатель публикует данные
14thread::spawn(move||{15data_writer.store(42,Ordering::Relaxed);16// Ordering::Release гарантирует, что ВСЕ предыдущие записи видны после этого момента
17flag_writer.store(true,Ordering::Release);18});1920letdata_reader=Arc::clone(&data);21letflag_reader=Arc::clone(&ready_flag);2223// Поток-Читатель ожидает флаг
24lethandle=thread::spawn(move||{25// Ordering::Acquire создает барьер чтения
26while!flag_reader.load(Ordering::Acquire){27thread::yield_now();28}29// Защищено барьером: data_reader гарантированно видит значение 42!
30println!("Прочитанные данные через Acquire/Release: {}",data_reader.load(Ordering::Relaxed));31});3233handle.join().unwrap();34}35
1. Барьеры памяти Acquire и Release
Ordering::Release создаёт барьер при записи: гарантирует, что все операции с памятью ДО записи не будут переупорядочены ПОСЛЕ неё.
Ordering::Acquire создаёт барьер при чтении: гарантирует, что операции ПОСЛЕ чтения не будут выполнены ДО него.
Пара Release + Acquire синхронизирует состояние между потоками без блокировок.
Результат выполнения:
1usestd::sync::atomic::{AtomicUsize,Ordering}; 2 3// Шаг 2: Атомарная операция Compare-And-Swap (CAS) через compare_exchange
4fnmain(){ 5letcurrent_val=AtomicUsize::new(10); 6 7letexpected=10;// Ожидаемое значение
8letnew_val=20;// Новое значение
910// Сравниваем: если текущее значение равно 10, заменяем его на 20:
11letresult=current_val.compare_exchange(12expected,13new_val,14Ordering::SeqCst,// Порядок при успехе
15Ordering::Relaxed// Порядок при ошибке
16);1718matchresult{19Ok(prev)=>println!("Успешный CAS: предыдущее значение было {prev}, стало {}",current_val.load(Ordering::Relaxed)),20Err(actual)=>println!("CAS провален: фактическое значение {actual}"),21}22}23
2. Атомарный Compare-And-Swap (CAS)
Функция compare_exchange является фундаментом всех lock-free структур данных (очередей, стеков, каналов).
Она проверяет ожидаемое значение и обновляет его ровно за одну атомарную операцию процессора.