Асинхронное программирование в Rust: полное руководство с примерами

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

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

Начать курс

Асинхронное программирование в Rust: от основ до продвинутых техник

Современные приложения требуют высокой производительности и эффективной работы с I/O операциями. Rust, известный своей скоростью и безопасностью, предлагает мощную модель асинхронного программирования. В этом руководстве мы детально разберем, как использовать async/await, библиотеку tokio и другие инструменты для создания быстрых и надежных асинхронных программ.

Что такое асинхронное программирование в Rust?

Асинхронное программирование — это парадигма, позволяющая выполнять множество задач одновременно без создания потоков ОС. Вместо блокировки потока при ожидании I/O (чтение файла, HTTP-запрос), асинхронная функция "приостанавливается" и передает управление другим задачам. Rust реализует это через Futures и механизм async/await.

Ключевые концепции:

  • Future — трейт, представляющий асинхронную операцию, которая может завершиться позже.
  • async/await — синтаксический сахар для работы с Futures.
  • Runtime (исполнитель) — среда выполнения, которая опрашивает Futures и запускает их. Самый популярный — Tokio.
  • Task — легковесная единица работы, управляемая Runtime.

Первая асинхронная программа на Rust

Для начала добавьте зависимости в Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Теперь напишем простой асинхронный HTTP-клиент:

use reqwest;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Todo {
    userId: u32,
    id: u32,
    title: String,
    completed: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box> {
    // Асинхронный GET-запрос
    let response = reqwest::get("https://jsonplaceholder.typicode.com/todos/1")
        .await?;  // await приостанавливает выполнение, пока не придет ответ
    
    let todo: Todo = response.json().await?;  // Асинхронная десериализация
    
    println!("Получена задача: {:#?}", todo);
    Ok(())
}

Обратите внимание на макрос #[tokio::main] — он превращает обычную функцию main в асинхронную точку входа, запуская Runtime Tokio.

Создание асинхронных функций и работа с задачами

Вы можете создавать собственные асинхронные функции с помощью ключевого слова async fn:

use std::time::Duration;
use tokio::time::sleep;

async fn simulate_work(id: u32, duration_secs: u64) -> String {
    println!("Задача {} началась", id);
    sleep(Duration::from_secs(duration_secs)).await;  // Асинхронная задержка
    println!("Задача {} завершилась", id);
    format!("Результат задачи {}", id)
}

#[tokio::main]
async fn main() {
    let start = std::time::Instant::now();
    
    // Создаем асинхронные задачи (tasks)
    let task1 = tokio::spawn(simulate_work(1, 2));
    let task2 = tokio::spawn(simulate_work(2, 1));
    let task3 = tokio::spawn(simulate_work(3, 3));
    
    // Ждем завершения всех задач
    let results = tokio::join!(task1, task2, task3);
    
    // Обрабатываем результаты
    for result in &results {
        match result {
            Ok(msg) => println!("{}", msg),
            Err(e) => eprintln!("Ошибка: {}", e),
        }
    }
    
    println!("Общее время: {:?}", start.elapsed());
    // Выполнится за ~3 секунды, а не за 6!
}

Функция tokio::spawn создает независимую асинхронную задачу, которая выполняется параллельно с другими. tokio::join! ожидает завершения всех переданных фьючерсов.

Продвинутые техники: каналы, селекторы и стримы

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

Для передачи данных между задачами используйте каналы tokio::sync::mpsc:

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::(32);
    
    let producer = tokio::spawn(async move {
        for i in 1..=5 {
            let msg = format!("Сообщение {}", i);
            tx.send(msg).await.unwrap();
            tokio::time::sleep(Duration::from_millis(500)).await;
        }
    });
    
    let consumer = tokio::spawn(async move {
        while let Some(msg) = rx.recv().await {
            println!("Получено: {}", msg);
        }
    });
    
    tokio::join!(producer, consumer);
}

Селектор (tokio::select!)

Макрос select! позволяет ждать несколько фьючерсов и реагировать на первый завершившийся:

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

async fn fetch_data() -> &'static str {
    sleep(Duration::from_secs(2)).await;
    "Данные загружены"
}

async fn timeout_handler() -> &'static str {
    sleep(Duration::from_secs(1)).await;
    "Таймаут!"
}

#[tokio::main]
async fn main() {
    tokio::select! {
        result = fetch_data() => println!("{}", result),
        timeout = timeout_handler() => println!("{}", timeout),
    }
    // Выведет "Таймаут!", так как он завершится быстрее
}

Асинхронные стримы (Stream)

Stream — асинхронный аналог итератора. Позволяет обрабатывать последовательность значений асинхронно:

use tokio_stream::StreamExt;

async fn generate_numbers() -> impl tokio_stream::Stream {
    tokio_stream::iter(1..=5)
}

#[tokio::main]
async fn main() {
    let mut stream = generate_numbers();
    
    while let Some(num) = stream.next().await {
        println!("Получено число: {}", num);
    }
    
    // Или с использованием collect
    let numbers: Vec = generate_numbers().                

Блоги

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