Конкурентность и многопоточность в Rust: безопасное программирование

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Конкурентность и многопоточность в 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 — вы удивитесь, как редко возникают ошибки синхронизации, если компилятор уже всё проверил.

Блоги

Рекомендаций по книгам