Конкурентность и многопоточность в Rust: пишем безопасный параллельный код
Rust — один из немногих языков, который решает проблему гонок данных (data races) на этапе компиляции. Благодаря системе владения (ownership) и заимствования (borrowing), конкурентный код в Rust становится не только быстрым, но и безопасным. В этой статье мы разберём основы многопоточности в Rust: создание потоков, обмен данными через каналы и мьютексы, а также познакомимся с моделью «Fearless Concurrency».
1. Основы многопоточности: std::thread
Rust предоставляет встроенную поддержку потоков через модуль std::thread. Создать новый поток можно с помощью функции thread::spawn, которая принимает замыкание.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("Поток: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("Главный поток: {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // ожидаем завершения потока
}
Метод join() блокирует главный поток до тех пор, пока дочерний поток не завершится. Без него программа может завершиться раньше, чем выполнится поток.
2. Передача данных между потоками: каналы (mpsc)
Для обмена сообщениями между потоками в Rust используется модель многопроизводительный, однопотребительский канал (mpsc — multiple producer, single consumer). Это реализовано в модуле std::sync::mpsc.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("Привет из потока!");
tx.send(val).unwrap();
// val уже перемещён, использовать его нельзя
});
let received = rx.recv().unwrap();
println!("Получено: {}", received);
}
Обратите внимание на ключевое слово move в замыкании — оно передаёт владение переменной tx в новый поток. Метод send() возвращает Result, который может быть Err, если получатель был закрыт.
Для создания нескольких производителей можно клонировать tx:
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx.send("Сообщение 1").unwrap();
});
thread::spawn(move || {
tx1.send("Сообщение 2").unwrap();
});
for received in rx {
println!("Получено: {}", received);
}
3. Общая память: Mutex и Arc
Каналы удобны для передачи данных, но иногда нужно разделять состояние между потоками. Для этого используется Mutex (взаимное исключение) в связке с Arc (атомарный счётчик ссылок).
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!("Результат: {}", *counter.lock().unwrap());
}
Почему Arc, а не Rc? Обычный Rc не является потокобезопасным, так как его счётчик ссылок не атомарен. Arc использует атомарные операции, что делает его безопасным для многопоточности.
Важно: Mutex блокирует поток при вызове lock(). После выхода из области видимости блокировка автоматически снимается (благодаря трейту Drop).
4. Модель Fearless Concurrency и Send/Sync
Главная сила Rust в конкурентности — это трейты Send и Sync:
- Send — тип может быть безопасно передан в другой поток. Почти все примитивные типы реализуют Send, кроме
Rcи некоторых других. - Sync — на тип можно безопасно ссылаться из нескольких потоков. Типы, которые реализуют Sync, гарантируют, что их использование не приведёт к гонке данных.
Компилятор автоматически проверяет эти трейты. Если вы попытаетесь передать Rc в другой поток, код не скомпилируется:
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(5);
// Ошибка: Rc<i32> не реализует Send
thread::spawn(move || {
println!("{}", data);
});
}
Эта проверка на этапе компиляции и называется Fearless Concurrency — вы можете смело писать многопоточный код, зная, что компилятор не пропустит опасные ситуации.
Заключение
Конкурентность в Rust — это не просто возможность запустить несколько потоков. Это продуманная система, которая сочетает производительность системного языка с безопасностью, обычно доступной только в языках с управляемой памятью. Мы рассмотрели:
- Создание потоков через
std::thread - Обмен сообщениями через каналы
mpsc - Разделение состояния с помощью
MutexиArc - Систему трейтов
SendиSync
Для более сложных сценариев (например, асинхронный ввод-вывод) в Rust существует экосистема async/await с библиотеками tokio и async-std. Но даже базовые средства многопоточности делают Rust одним из лучших языков для написания надёжного параллельного кода.
Попробуйте написать свой первый многопоточный проект на Rust — вы удивитесь, как редко возникают ошибки синхронизации, если компилятор уже всё проверил.