Как работает модель конкурентности в Rust?
Модель конкурентности в Rust построена на принципе безопасности на этапе компиляции, благодаря чему разработчики могут писать параллельный и многопоточный код без гонок данных и без необходимости в сборщике мусора. Это достигается за счёт системы владения, заимствования и времён жизни, которая применяется не только к одиночным значениям, но и к потокам выполнения.
Rust предлагает низкоуровневые примитивы конкурентности, которые предоставляют максимальный контроль, а также высокоуровневые абстракции, которые упрощают разработку многопоточного кода.
Базовые принципы конкурентности в Rust
В основе модели лежат несколько ключевых понятий:
- **Безопасный доступ к данным из нескольких потоков
** - **Отсутствие гонок данных (data race)
** - **Компилятор проверяет, что доступ к данным синхронизирован
** - **Параллелизм реализуется через std::thread, Send и Sync
**
Параллельное выполнение через std::thread
Самый простой способ реализовать конкурентность — создать новый поток:
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Привет из потока!");
});
handle.join().unwrap(); // дожидаемся завершения
}
Функция spawn создаёт новый поток, который выполняет переданное замыкание.
Передача данных между потоками и тип Send
В Rust любой тип, который можно безопасно передать между потоками, реализует маркерный трейд Send.
-
Типы вроде i32, Vec<T> или Box<T> реализуют Send по умолчанию.
-
Пользовательские типы также могут быть Send, если все их поля — Send.
fn main() {
let v = vec!\[1, 2, 3\];
let handle = std::thread::spawn(move || {
println!("{:?}", v); // move нужно, чтобы владение перешло потоку
});
handle.join().unwrap();
}
Без move компилятор не позволит использовать v в потоке, так как владение останется в основном потоке.
Совместное использование данных: Sync и Arc
Когда нужно разделять данные между потоками, например, с возможностью одновременного чтения, используется тип Arc<T> (атомарный счётчик ссылок).
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec!\[1, 2, 3\]);
let mut handles = vec!\[\];
for _ in 0..3 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
println!("{:?}", data);
});
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
}
Трейд Sync означает, что тип можно одновременно читать из нескольких потоков (если у него только &T).
Модификация разделяемых данных: Mutex
Чтобы изменять данные из нескольких потоков, используется мьютекс:
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());
}
-
Mutex<T> обеспечивает безопасный доступ к данным.
-
Комбинация Arc<Mutex
используется для совместного использования изменяемого состояния.
Отсутствие гонок данных
Rust не допускает гонок данных на этапе компиляции. Это означает, что:
-
Нельзя одновременно иметь несколько изменяемых ссылок на один и тот же объект.
-
Нельзя передать объект в поток, если он не реализует Send.
-
Нельзя поделиться ссылкой на объект между потоками, если он не реализует Sync.
В безопасном коде Rust невозможно создать гонку данных — для этого нужно использовать unsafe.
Каналы и message passing
Для безопасной и удобной коммуникации между потоками Rust предлагает каналы (channel):
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("Привет!").unwrap();
});
let received = rx.recv().unwrap();
println!("Получено: {}", received);
}
-
Каналы основаны на message-passing concurrency, как в Erlang или Go.
-
mpsc — значит “multiple producers, single consumer”.
Каналы позволяют избежать необходимости в общей памяти и мьютексах.
Статические гарантии конкурентности: Send и Sync
Трейты Send и Sync — ключевые маркеры конкурентности в Rust.
-
Send означает, что тип можно передать между потоками (T в thread::spawn(move || ...)).
-
Sync означает, что &T можно совместно использовать между потоками.
Примеры:
-
i32: Send + Sync
-
Vec<T>: Send + Sync, если T: Send + Sync
-
Rc<T>: не Send, не Sync
-
Arc<T>: Send + Sync, если T соответствующий
Компилятор Rust проверяет, реализует ли тип нужные трейты перед передачей между потоками.
Асинхронность и async/await
Параллельно с многопоточностью в Rust развивается модель асинхронного программирования через async/await. Она построена на безопасной реализации Future и экосистеме, включая tokio, async-std, smol.
Ключевое отличие от потоков: async работает в одном потоке, но переключается между задачами, не блокируя выполнение.
Асинхронный код также использует владение и заимствование, и проверяется компилятором на корректность. Например, async-замыкание не может захватывать данные без владения, если они будут использованы после await.