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

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

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

Начать курс

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

Современные программы редко работают в однопоточном режиме. Обработка запросов, работа с файлами, сетевые операции — всё это требует параллельного выполнения задач. Rust предлагает уникальный подход к конкурентности: он гарантирует безопасность памяти на этапе компиляции, предотвращая гонки данных. В этой статье мы разберём, как работать с потоками, каналами, мьютексами и асинхронностью в Rust.

1. Основы многопоточности: создание и управление потоками

В Rust потоки создаются через стандартную библиотеку std::thread. Каждый поток выполняется независимо, но компилятор строго следит за временем жизни переменных.

Создание потока

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(100));
        }
    });

    for i in 1..5 {
        println!("Главный поток: {}", i);
        thread::sleep(Duration::from_millis(100));
    }

    handle.join().unwrap(); // Ожидаем завершения потока
}

Захват владения в замыканиях

Если поток использует переменные из внешней области, нужно передать владение через move:

let data = vec![1, 2, 3];

let handle = thread::spawn(move || {
    println!("Данные: {:?}", data);
});

handle.join().unwrap();
// println!("{:?}", data); // Ошибка! data перемещена

2. Обмен данными: мьютексы и блокировки

Для безопасного доступа к общим данным из нескольких потоков Rust использует 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()); // 10
}

Важно: Mutex автоматически разблокируется при выходе из области видимости. Это предотвращает deadlock (взаимную блокировку) на уровне компиляции.

RwLock — для частого чтения

Если данные редко изменяются, но часто читаются, используйте RwLock:

use std::sync::RwLock;

let lock = RwLock::new(5);

{
    let r1 = lock.read().unwrap();
    let r2 = lock.read().unwrap(); // Можно одновременно читать
    println!("{} {}", r1, r2);
}

{
    let mut w = lock.write().unwrap();
    *w += 1; // Запись блокирует чтение
}

3. Каналы: передача сообщений между потоками

Rust поддерживает модель акторов через каналы (std::sync::mpsc). Это позволяет потокам обмениваться данными без разделяемой памяти.

Пример с каналом

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec!["hello", "from", "the", "thread"];
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(std::time::Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Получено: {}", received);
    }
}

Множество отправителей

С помощью mpsc::Sender можно клонировать отправителя для нескольких потоков:

let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();

thread::spawn(move || { tx1.send("Поток 1").unwrap(); });
thread::spawn(move || { tx.send("Поток 2").unwrap(); });

for msg in rx {
    println!("{}", msg);
}

4. Асинхронное программирование: async/await и Tokio

Для задач ввода-вывода (сеть, файлы) эффективнее использовать асинхронность. Rust поддерживает async/await на уровне языка, а популярный фреймворк Tokio предоставляет runtime.

Базовый пример с Tokio

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let task1 = async {
        sleep(Duration::from_secs(2)).await;
        println!("Задача 1 завершена");
    };

    let task2 = async {
        sleep(Duration::from_secs(1)).await;
        println!("Задача 2 завершена");
    };

    tokio::join!(task1, task2); // Обе выполняются параллельно
}

Асинхронные каналы

Tokio предоставляет собственные каналы для асинхронных потоков:

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);

    tokio::spawn(async move {
        tx.send("Асинхронное сообщение").await.unwrap();
    });

    if let Some(msg) = rx.recv().await {
        println!("{}", msg);
    }
}

Когда использовать async, а когда потоки?

  • Потоки — для CPU-интенсивных задач (расчёты, рендеринг).
  • Async — для I/O-операций (HTTP-запросы, чтение файлов).
  • Можно комбинировать: tokio::task::spawn_blocking для тяжёлых вычислений в асинхронном коде.

Заключение

Rust предлагает мощные и безопасные инструменты для параллельной работы:

  • Потоки — для классической многопоточности с контролем владения.
  • Mutex и RwLock — для разделяемого доступа без гонок данных.
  • Каналы — для передачи сообщений между

Блоги

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