Введение в асинхронное программирование в Rust
Асинхронное программирование — это подход, который позволяет вашему приложению эффективно выполнять несколько задач одновременно, не блокируя основной поток выполнения. В Rust этот подход реализован через модель async/await, которая обеспечивает безопасность памяти и высокую производительность, свойственные языку.
В отличие от традиционной многопоточности, асинхронный код работает в рамках одного или нескольких потоков, переключаясь между задачами во время ожидания (например, ввода-вывода или сетевых запросов). Это делает его идеальным для создания высоконагруженных веб-серверов, сетевых приложений и инструментов обработки данных.
В этой статье мы разберём основы асинхронного программирования в Rust: от ключевых концепций до практических примеров. Вы узнаете, как работают Future, async и await, познакомитесь с популярным рантаймом Tokio и напишете свой первый асинхронный проект.
Основные концепции: Future, async и await
Что такое Future?
В Rust асинхронная задача представлена трейтом Future. Future — это значение, которое может быть ещё не готово, но будет получено в будущем. Каждый Future имеет метод poll, который проверяет, завершена ли задача. Если задача ещё выполняется, возвращается Pending; если завершена — Ready(value).
Сам по себе Future не выполняет никакой работы. Он лишь описывает, что должно произойти. Для выполнения асинхронного кода требуется рантайм — исполнитель, который опрашивает Future и управляет их выполнением.
Ключевые слова async и await
Ключевое слово async преобразует обычную функцию в асинхронную, которая возвращает Future. Например:
async fn hello() -> String {
"Привет, мир!".to_string()
}
Чтобы получить результат из Future, используется .await. Он приостанавливает выполнение текущей функции до тех пор, пока Future не будет завершён:
let result = hello().await;
println!("{}", result);
Важно: .await можно использовать только внутри асинхронной функции или блока async {}.
Жизненный цикл асинхронной задачи
- Создание
Future(вызов асинхронной функции). - Передача
Futureрантайму для выполнения. - Рантайм опрашивает
Futureчерезpoll. - Если задача блокируется (ожидание I/O), рантайм переключается на другую задачу.
- Когда результат готов,
FutureвозвращаетReady.
Экосистема: Tokio и async-std
Для работы с асинхронным кодом в Rust используются асинхронные рантаймы. Два самых популярных — Tokio и async-std.
Tokio
Tokio — это промышленный стандарт для асинхронного программирования в Rust. Он предоставляет многопоточный планировщик, асинхронный ввод-вывод, таймеры, синхронизацию и сетевые протоколы. Большинство крупных проектов (например, Hyper, Tonic, Axum) построены на Tokio.
Чтобы начать работу с Tokio, добавьте зависимость в Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
Основной макрос для запуска асинхронного кода — #[tokio::main]:
#[tokio::main]
async fn main() {
println!("Hello from Tokio!");
}
async-std
async-std — альтернативный рантайм, который стремится быть максимально похожим на стандартную библиотеку Rust. Он проще для начинающих, но менее популярен в продакшене.
Пример с async-std:
use async_std::task;
async fn say_hello() {
println!("Hello!");
}
fn main() {
task::block_on(say_hello());
}
Для этого руководства мы будем использовать Tokio, так как он является де-факто стандартом в индустрии.
Практический пример: асинхронный веб-скрапинг
Давайте создадим простую программу, которая асинхронно загружает содержимое нескольких веб-страниц и выводит их заголовки. Это классический пример, демонстрирующий преимущества асинхронности.
Шаг 1: Настройка проекта
Создайте новый проект:
cargo new async_scraper
cd async_scraper
Добавьте в Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
Шаг 2: Написание кода
Создадим асинхронную функцию для загрузки одной страницы:
use reqwest;
use tokio;
async fn fetch_title(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?;
let body = response.text().await?;
// Простейший парсинг: ищем текст между <title> и </title>
let start = body.find("<title>").map(|i| i + 7);
let end = start.and_then(|s| body[s..].find("</title>").map(|e| s + e));
let title = match (start, end) {
(Some(s), Some(e)) => body[s..e].to_string(),
_ => "Заголовок не найден".to_string(),
};
Ok(title)
}
Теперь напишем основную функцию, которая запускает несколько задач параллельно:
#[tokio::main]
async fn main() {
let urls = vec![
"https://www.rust-lang.org",
"https://tokio.rs",
"https://crates.io",
];
let mut tasks = Vec::new();
for url in urls {
tasks.push(tokio::spawn(async move {
match fetch_title(url).