Многопоточность
Кратко
Конкурентность/параллелизм можно реализовать через очередь сообщений (Message-passing) или общее состояние (Shared-state)
Thread
При работе с конкурентностью нужно учитывать следующее
гонки данных (data race)
Состояния гонки (race conditions): thread обращаются к данным или ресурсам в несогласованном порядке
взаимная блокировка (deadlock)
Взаимные блокировки (deadlocks): два thread ждут, пока другой закончит использовать ресурс, который уже есть у первого, и оба не могут продолжить
трудно воспроизводимые BUG
Ошибки, которые возникают только в определённых ситуациях и которые трудно воспроизвести и надёжно исправить
Green Thread — это thread на уровне языка программирования; его функциональность не полностью совпадает с thread, который предоставляет операционная система
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| { // учтите: если замыканию нужны аргументы, нужно явно указать move
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1)); // в этот момент у операционной системы есть возможность переключиться на другие thread
}
});
handle.join().unwrap(); // join ждёт завершения всех thread (иначе дальнейший код не выполнится)
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
Message-passing
базовый вариант с Channel
use std::sync::mpsc; // multiple producer single consumer fn main() { let (tx, rx) = mpsc::channel(); // transmitter, receiver thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); // в этот момент val перемещается (moved ownership) }); let received = rx.recv().unwrap(); // при использовании try_recv блокировки не будет println!("Got: {}", received); }несколько producer/transmitter && приём нескольких сообщений
use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); // потому что дальше замыкание переместит ownership tx // поэтому здесь нужно заранее клонировать tx1, чтобы получить несколько producer let tx1 = tx.clone(); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); // обратите внимание: в отличие от предыдущего примера, здесь используется for-in loop for received in rx { println!("Got: {}", received); } }
Shared-state
Mutex (Mutual Exclusion, взаимное исключение); одно из его проявлений: в один момент времени только один thread может обращаться к определённым данным
Thread должен получить lock, чтобы обратиться к данным в mutex. lock — тоже часть mutex: он постоянно отслеживает, у кого есть право на взаимоисключающий доступ к данным в mutex. Поэтому мьютекс описывают как структуру, защищающую свои данные через систему блокировок.
Mutexes трудно использовать из-за следующих двух правил:
перед использованием данных нужно попытаться получить блокировку
Вы должны попытаться получить блокировку до того, как начнёте использовать данные.
когда работа с данными под защитой mutex закончена, блокировку нужно освободить, чтобы другие thread могли её получить
Когда вы закончили с данными, которые охраняет mutex, вы должны разблокировать данные, чтобы другие thread могли получить блокировку.
Управлять mutexes очень трудно, поэтому многие предпочитают channels. Однако система типов и правила владения Rust гарантируют, что мы не получим и не освободим блокировку неправильно
базовый вариант с Mutex
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); // вызов lock получает блокировку *num = 6; } println!("m = {:?}", m); }Mutex<T>— это умный указатель; точнее,lockвозвращаетLockResult, а затемunwrapвозвращает умный указательMutexGuard;MutexGuardреализует трейтDeref, указывая на внутренние данные, и трейтDrop, автоматически освобождая блокировку, когдаMutexGuardвыходит из области видимости. Поэтому мы не рискуем забыть освободить блокировку и заблокировать мьютекс для других thread, поскольку освобождение происходит автоматически.совместное использование Mutex несколькими thread
Atomic Reference Counted Type — можно понимать как
Rc<T>с атомарными операциямиuse std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
Send & Sync trait
Типы, реализующие трейт Send, могут передавать ownership между thread
Типы, реализующие трейт Sync, могут использоваться несколькими thread одновременно
Видно, что Sync включает в себя свойства Send
Источник
…
Атомарный подсчёт ссылок с Arc<T>
К счастью, Arc<T> — это тип, похожий на Rc<T>, но безопасный в конкурентных ситуациях. a означает atomic, то есть это тип с атомарным подсчётом ссылок. Атомарные операции — дополнительный вид примитивов конкурентности, который мы здесь подробно не разбираем: см. документацию стандартной библиотеки для
std::sync::atomic
. Сейчас достаточно знать, что атомарные типы работают как примитивные, но безопасны для совместного использования между thread.
Тогда может возникнуть вопрос: почему не все примитивные типы атомарны и почему типы стандартной библиотеки не реализованы по умолчанию через Arc<T>? Причина в том, что потокобезопасность сопряжена с потерей производительности, которую хочется оплачивать только тогда, когда это действительно нужно. Если вы выполняете операции со значениями в одном thread, код может работать быстрее, если не обязан обеспечивать гарантии атомарности.
Вернёмся к примеру: у Arc<T> и Rc<T> одинаковый API, поэтому мы исправим программу, изменив строку use, вызов new и вызов clone. Код из листинга 16-15 наконец скомпилируется и запустится:
…