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

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

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

Начать курс

Введение в асинхронное программирование в Rust

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

В этом руководстве мы разберем:

  • Основы async/await в Rust
  • Работу с крейтом tokio — самым популярным асинхронным рантаймом
  • Создание и комбинирование Future
  • Практические примеры: асинхронный HTTP-клиент и таймеры

Статья ориентирована на разработчиков, знакомых с базовым синтаксисом Rust. Если вы только начинаете — рекомендуем сначала изучить основы языка на PythonLib.

Основы async/await и Future в Rust

Что такое Future?

Future — это трейт, представляющий асинхронную операцию, которая может завершиться в будущем. В Rust Future является ленивым: он ничего не делает, пока его не опросят (poll). За опрос отвечает асинхронный рантайм (например, Tokio).

Простой пример асинхронной функции:

use std::time::Duration;

async fn say_hello() {
    println!("Привет из асинхронного мира!");
    tokio::time::sleep(Duration::from_secs(1)).await;
    println!("Прошла 1 секунда");
}

#[tokio::main]
async fn main() {
    say_hello().await;
}

Ключевое слово async превращает функцию в Future. Вызов .await приостанавливает выполнение текущей задачи до завершения Future, не блокируя поток.

Жизненный цикл асинхронной задачи

Когда вы вызываете .await, происходит следующее:

  1. Рантайм опрашивает Future (вызывает метод poll())
  2. Если Future готов — возвращается результат
  3. Если Future ожидает (например, сетевая операция) — рантайм переключается на другую задачу
  4. Когда ресурс становится доступен, рантайм возобновляет задачу

Важная особенность Rust: .await может быть вызван только внутри асинхронной функции или блока async {}. Попытка использовать его в синхронном контексте приведет к ошибке компиляции.

Практическая работа с Tokio

Установка и настройка

Tokio — это асинхронный рантайм для Rust, предоставляющий планировщик задач, таймеры, сетевые сокеты и многое другое. Для начала добавьте в Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }

Флаг features = ["full"] включает все возможности Tokio. Для продакшена лучше выбирать только необходимые фичи.

Запуск нескольких асинхронных задач

Одно из главных преимуществ асинхронности — параллельное выполнение множества задач. Используйте tokio::spawn для запуска задач в фоне:

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

async fn task(name: &str, seconds: u64) {
    println!("Задача {} началась", name);
    sleep(Duration::from_secs(seconds)).await;
    println!("Задача {} завершена через {} сек", name, seconds);
}

#[tokio::main]
async fn main() {
    let handle1 = tokio::spawn(task("A", 2));
    let handle2 = tokio::spawn(task("B", 1));

    // Ждем завершения обеих задач
    let _ = tokio::join!(handle1, handle2);
    println!("Все задачи выполнены");
}

Макрос tokio::join! ожидает завершения всех переданных Future. Если одна задача завершится раньше, выполнение не прервется — дождемся остальных.

Обработка ошибок в асинхронном коде

Асинхронные функции могут возвращать Result. Используйте ? оператор внутри асинхронного контекста:

use tokio::fs::File;
use tokio::io::AsyncReadExt;

async fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path).await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    Ok(contents)
}

#[tokio::main]
async fn main() {
    match read_file("data.txt").await {
        Ok(data) => println!("Файл: {}", data),
        Err(e) => eprintln!("Ошибка: {}", e),
    }
}

Комбинирование и трансформация Future

Последовательное выполнение

Используйте .await для последовательного выполнения:

async fn step1() -> u32 { 10 }
async fn step2(value: u32) -> u32 { value * 2 }

#[tokio::main]
async fn main() {
    let result = step2(step1().await).await;
    println!("Результат: {}", result); // 20
}

Параллельное выполнение с select!

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

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

#[tokio::main]
async fn main() {
    let slow = sleep(Duration::from_secs(5));
    let fast = sleep(Duration::from_secs(2));

    tokio::select! {
        _ = slow => println!("Медленная задача завершена"),
        _ = fast => println!("Быстрая задача завершена первой"),
    }
}

Это полезно для реализации тайм-аутов или обработки первого ответа от нескольких серверов.

Трансформация с помощью комбинаторов

Крейт futures предоставляет комбинаторы для работы с Future:

use futures::future::{self, FutureExt};

async fn fetch_data(id: u32) -&                

Блоги

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