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

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

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

Начать курс

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

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

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

Основы async/await и Futures

Что такое Future?

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

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct MyFuture {
    ready: bool,
}

impl Future for MyFuture {
    type Output = u32;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
        if self.ready {
            Poll::Ready(42)
        } else {
            // Регистрируем waker для повторного опроса
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

async/await — синтаксический сахар

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

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

async fn fetch_data() -> String {
    // Имитация сетевого запроса
    sleep(Duration::from_secs(2)).await;
    String::from("Данные получены")
}

#[tokio::main]
async fn main() {
    let result = fetch_data().await;
    println!("{}", result);
}

Жизненные циклы и borrow checker

Одна из особенностей Rust — строгий контроль заимствований. В асинхронном коде это особенно важно, так как Future может жить дольше, чем заимствованные данные. Используйте async move для захвата владения:

async fn process_data(data: Vec<u32>) -> u32 {
    data.iter().sum()
}

#[tokio::main]
async fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let sum = process_data(numbers).await;
    println!("Сумма: {}", sum);
}

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

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

Tokio — самый популярный асинхронный рантайм для Rust. Добавьте в Cargo.toml:

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

Создание асинхронного TCP-сервера

Рассмотрим простой эхо-сервер, который принимает соединения и отправляет обратно полученные данные:

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Сервер запущен на порту 8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        println!("Новое соединение от: {}", addr);

        tokio::spawn(async move {
            let mut buf = [0; 1024];
            loop {
                let n = socket.read(&mut buf).await.unwrap();
                if n == 0 { break; } // Соединение закрыто
                socket.write_all(&buf[..n]).await.unwrap();
            }
        });
    }
}

Параллельные задачи и JoinSet

Для управления множеством задач используйте JoinSet или tokio::join!:

use tokio::task::JoinSet;

async fn task(id: u32) -> u32 {
    tokio::time::sleep(tokio::time::Duration::from_secs(id as u64)).await;
    id * 10
}

#[tokio::main]
async fn main() {
    let mut set = JoinSet::new();

    for id in 1..=5 {
        set.spawn(task(id));
    }

    while let Some(result) = set.join_next().await {
        match result {
            Ok(value) => println!("Задача завершена: {}", value),
            Err(e) => eprintln!("Ошибка: {}", e),
        }
    }
}

Обработка ошибок и каналы

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

Каналы позволяют безопасно передавать данные между задачами:

use tokio::sync::mpsc;

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

    // Отправитель
    tokio::spawn(async move {
        for i in 0..5 {
            tx.send(format!("Сообщение {}", i)).await.unwrap();
            tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
        }
    });

    // Получатель
    while let Some(msg) = rx.recv().await {
        println!("Получено: {}", msg);
    }
}

Обработка ошибок с anyhow

Для упрощения работы с ошибками используйте крейт anyhow:

use anyhow::Result;

async fn risky_operation() -> Result<String> {
    let content = tokio::fs::read_to_string("config.toml").await?;
    Ok(content)
}

#[tokio::main]
async fn main() -> Result<()> {
    match risky_operation().await {
        Ok(data) => println!("Конфиг: {}", data),
        Err(e) => eprintln!("Ошибка чтения: {}", e),
    }
    Ok(())
}

Заключение

Асинхронное программирование в Rust — мощный инструмент для создания высокопроизводительных приложений. Мы рассмотрели основы async/await, работу с Tokio

Блоги

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