Конкурентность и многопоточность в Rust: продвинутые техники

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

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

Начать курс

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

Конкурентность и многопоточность — одни из самых мощных и одновременно сложных аспектов системного программирования. Rust предлагает уникальный подход: гарантии безопасности памяти на этапе компиляции, исключая состояние гонки (data race) и висячие указатели. В этой статье мы разберём продвинутые техники работы с конкурентностью в Rust, включая async/await, библиотеку Tokio, трейты Send и Sync, а также низкоуровневые примитивы синхронизации.

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

1. Модели конкурентности: потоки ОС и асинхронность

Rust поддерживает две основные модели: потоки операционной системы (OS threads) и асинхронные задачи (async tasks). Выбор между ними зависит от сценария: для CPU-интенсивных задач лучше подходят потоки, для I/O-bound — async.

Потоки ОС (std::thread)

Создание потока в Rust тривиально, но требует внимания к владению данными:

use std::thread;

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

let handle = thread::spawn(move || {
    println!("{:?}", data);
});

handle.join().unwrap();

Ключевой момент — ключевое слово move. Оно передаёт владение замыканию. Если нужно разделить данные между потоками, используйте Arc<Mutex<T>>.

Асинхронное программирование (async/await)

Асинхронный код работает в рамках одного потока, переключаясь между задачами во время ожидания I/O. Это снижает накладные расходы на переключение контекста. Пример простой асинхронной функции:

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

#[tokio::main]
async fn main() {
    let task1 = async_task("Задача 1");
    let task2 = async_task("Задача 2");

    tokio::join!(task1, task2);
}

async fn async_task(name: &str) {
    println!("{} началась", name);
    sleep(Duration::from_secs(1)).await;
    println!("{} завершена", name);
}

Библиотека tokio — стандарт для асинхронного Rust. Она предоставляет многопоточный планировщик (work-stealing scheduler), который эффективно распределяет задачи по ядрам CPU.

2. Трейты Send и Sync: безопасность конкурентности

Два ключевых трейта обеспечивают безопасность конкурентности в Rust:

  • Send — тип можно безопасно передавать между потоками.
  • Sync — на тип можно безопасно ссылаться из нескольких потоков одновременно.

Большинство примитивов Rust автоматически реализуют эти трейты, если их компоненты их реализуют. Например, Arc<Mutex<i32>> — Send и Sync, а Rc<RefCell<i32>> — нет.

Рассмотрим пример, который не скомпилируется из-за нарушения Send:

use std::rc::Rc;
use std::thread;

let data = Rc::new(5);

thread::spawn(move || {
    println!("{}", data);
}); // Ошибка: Rc<i32> не реализует Send

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

3. Продвинутая синхронизация: каналы, мьютексы и RwLock

Для обмена данными между потоками Rust предлагает несколько примитивов. Рассмотрим наиболее мощные.

Каналы (mpsc)

Многопоточные каналы (multi-producer, single-consumer) позволяют отправлять сообщения из нескольких потоков в один приёмник:

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

let (tx, rx) = mpsc::channel();

for i in 0..3 {
    let tx_clone = tx.clone();
    thread::spawn(move || {
        tx_clone.send(i).unwrap();
    });
}

drop(tx); // Закрываем передатчик

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

Каналы идеальны для архитектуры "производитель-потребитель" и позволяют избежать блокировок при передаче данных между потоками.

Mutex и RwLock

Для разделяемого доступа к данным с блокировками:

use std::sync::{Arc, Mutex};

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

RwLock (Read-Write Lock) эффективнее, когда читателей много, а писателей мало. Он позволяет множеству читателей одновременно, но блокирует всех при записи.

4. Продвинутые паттерны: work-stealing и scoped threads

Для максимальной производительности в Rust существуют продвинутые механизмы.

Scoped threads (std::thread::scope)

Позволяют заимствовать данные из внешней области видимости без Arc:

use std::thread;

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

thread::scope(|s| {
    s.spawn(|| {
        data.push(4);
    });
    s.spawn(|| {
        data.push(5);
    });
});

println!("{:?}", data); // [1, 2, 3, 4, 5]

Это безопасно, так как все потоки завершаются до выхода из scope, и компилятор гарантирует отсутствие висячих ссылок.

Work-stealing с Tokio

Tokio использует планировщик с "воровством работы" (work-stealing). Каждый поток имеет локальную очередь задач, и если она пуста, поток "крадёт" задачи у других. Это даёт высокую загрузку CPU без ручного управления.

use tokio::task;

#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    let mut handles = vec![];

    for i in 0..100 {
        handles.push(task::spawn(async move {
            // CPU-интенсивная работа
            let result = heavy_computation(i);
            result
        }));
    }

    for handle in                

Блоги

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