Как работает модель конкурентности в Rust?

Модель конкурентности в Rust построена на принципе безопасности на этапе компиляции, благодаря чему разработчики могут писать параллельный и многопоточный код без гонок данных и без необходимости в сборщике мусора. Это достигается за счёт системы владения, заимствования и времён жизни, которая применяется не только к одиночным значениям, но и к потокам выполнения.

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

Базовые принципы конкурентности в Rust

В основе модели лежат несколько ключевых понятий:

  1. **Безопасный доступ к данным из нескольких потоков
    **
  2. **Отсутствие гонок данных (data race)
    **
  3. **Компилятор проверяет, что доступ к данным синхронизирован
    **
  4. **Параллелизм реализуется через 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.