механизм без навязанной политикиmechanism without a forced policy
async fn и async-блоки создают значение, реализующее трейт Future — ленивую, poll-based машину состояний, представляющую вычисление, которое может быть ещё не готово. Стандартная библиотека даёт трейт Future и синтаксис async/await, но сознательно не даёт executor'а — его предоставляет отдельный крейт (tokio, async-std, smol).async fn and async blocks produce a value implementing the Future trait — a lazy, poll-based state machine representing a computation that may not be ready yet. The standard library ships the Future trait and async/await syntax, but deliberately ships no executor — that comes from a separate crate (tokio, async-std, smol).
Вызов async fn НЕ запускает выполнение её тела вообще — он возвращает Future, которая ничего не делает, пока что-то активно не начнёт её продвигать, многократно вызывая .poll() (который возвращает Poll::Ready(value) при завершении или Poll::Pending иначе). Критично: стандартная библиотека Rust содержит трейт Future и синтаксис async/await, но сознательно НЕ содержит async runtime/executor вообще — довести Future до завершения требует отдельного крейта-исполнителя (tokio, async-std, smol и т.д.), осознанное дизайн-решение, отличающее Rust почти от любого другого mainstream-языка с async/await (JS, C#, Python поставляют runtime вместе с языком/стандартной библиотекой).Calling an async fn does NOT start running its body at all — it returns a Future that does nothing until something actively drives it forward by repeatedly calling .poll() (returning Poll::Ready(value) when done or Poll::Pending otherwise). Critically, Rust's standard library ships the Future trait and the async/await syntax but deliberately ships NO async runtime/executor at all — running a Future to completion requires a separate executor crate (tokio, async-std, smol, etc.), a deliberate design choice distinguishing Rust from essentially every other mainstream language with async/await (JS, C#, Python all bundle a runtime with the language/standard library).
Заметка про edition: сам синтаксис async/.await появился в языке только с выходом Rust 2018 — до этого писать асинхронный код можно было только через ручную реализацию Future (комбинаторный стиль поверх крейта futures 0.1), без какого-либо специального синтаксиса вообще. Это одна из главных причин, по которой edition 2018 стал крупным вехой в истории языка — не столько сам механизм Future (он появился раньше), сколько удобный поверхностный синтаксис для его использования.Edition note: the async/.await syntax itself only landed in the language with the Rust 2018 release — before that, writing asynchronous code meant manually implementing Future (a combinator style layered on the futures 0.1 crate), with no dedicated syntax at all. This is one of the main reasons edition 2018 was a major milestone in the language's history — not so much the Future mechanism itself (which predates it), but the convenient surface syntax for using it.
Отсутствие встроенного runtime позволяет модели async обслуживать принципиально разные домены (embedded-системы без OS-уровневых потоков вообще, высоконагруженный многопоточный серверный код, однопоточные WASM-окружения) без навязывания компромиссов одного runtime всем сразу — выбор, напрямую обеспеченный тем, что Future — это обычный трейт с маленьким, чётко определённым интерфейсом (poll), а не встроенная в язык, привязанная к runtime конструкция.This "no bundled runtime" design lets the async model serve fundamentally different domains (embedded systems with no OS-level threading at all, high-throughput multi-threaded server code, single-threaded WASM environments) without forcing one runtime's tradeoffs on all of them — a choice directly enabled by Future being a plain trait with a small, well-defined interface (poll) rather than a runtime-coupled language builtin.
Это отражает на уровне философии дизайна языка общее предпочтение Rust: "давать механизм, пусть библиотеки задают политику" — та же логика, что стоит за отсутствием навязанного базового класса "Object" у трейтов и отсутствием навязанного "во что собирать" у итераторов.This mirrors, at a language-design-philosophy level, Rust's overall preference for "provide the mechanism, let libraries provide policy" — the same reasoning behind traits having no forced base "Object" class, and iterators having no forced "collect into what" default.
На практике это означает, что чтение и написание реального асинхронного Rust-кода требует знания, под какой executor он написан (обычно tokio) — экосистемные крейты вроде tokio живут в разделе ecosystem/ этой базы знаний, отдельно от языковых основ, описанных здесь.Practically, this means reading/writing real Rust async code requires knowing which executor you're targeting (usually tokio in practice) — ecosystem crates like tokio live in this knowledge base's ecosystem section, separate from the language fundamentals covered here.
Объявление async fn fetch_data() -> String { ... } ничего не выполняет само по себе: let future = fetch_data(); лишь создаёт значение-Future. Продвигает её к завершению .await внутри другой async fn — примерно "опрашивай эту Future снова и снова (отдавая управление обратно executor'у между опросами), пока она не станет Ready". .await можно использовать только внутри async-контекста.Declaring async fn fetch_data() -> String { ... } executes nothing by itself: let future = fetch_data(); only creates a Future value. What drives it to completion is .await inside another async fn — roughly "poll this Future repeatedly (yielding control back to the executor between polls) until it's Ready". .await can only be used inside an async context.
async fn fetch_data() -> String {
// ... сетевой запрос и т.п.// ... a network request or similar
String::from("data")
}
async fn process() {
let future = fetch_data(); // ничего не выполнилось, это просто значение Future// nothing has run yet, this is just a Future value
let data = future.await; // теперь она опрашивается до готовности// now it's polled until ready
println!("{}", data);
}
Реальная программа мостит синхронную точку входа main в асинхронный мир через макрос конкретного runtime, например tokio:A real program bridges its synchronous entry point into the async world via a specific runtime's macro, e.g. tokio:
// глубоко в API tokio здесь не погружаемся — это тема ecosystem/// not diving deep into tokio's own API here — that's an ecosystem/ topic
#[tokio::main]
async fn main() {
process().await;
}
Зависимые асинхронные операции компонуются обычным на вид последовательным синтаксисом, оставаясь неблокирующими под капотом.Dependent async operations compose with ordinary-looking sequential syntax despite being non-blocking under the hood.
async fn run() {
let a = fetch_a().await;
let b = fetch_b(a).await; // зависит от a, поэтому идёт строго после// depends on a, so it strictly follows it
println!("{}", b);
}
Когда две операции независимы, их можно выполнить конкурентно вместо последовательного ожидания — точный механизм совместного выполнения (join!-подобный макрос) зависит от конкретного runtime/крейта, это не единый стабильный синтаксис языка.When two operations are independent, they can run concurrently instead of being awaited sequentially — the exact joining mechanism (a join!-style macro) depends on the specific runtime/crate in use, it isn't one fixed language-level syntax.
// концептуально: обе future продвигаются конкурентно, а не одна за другой// conceptually: both futures are driven concurrently, not one after another
let (a, b) = futures::join!(fetch_from_source_a(), fetch_from_source_b());
Future в состоянии Pending не блокирует поток ОС вообще — executor может кооперативно выполнять тысячи ожидающих Future на маленьком пуле потоков, поэтому async — стандартный выбор для высококонкурентных I/O-bound серверов, где создание одного потока ОС на соединение не масштабируется.A Future that's Pending doesn't block an OS thread at all — the executor can run thousands of pending Futures cooperatively on a small thread pool, which is why async is the standard choice for high-concurrency I/O-bound servers where spawning one OS thread per connection wouldn't scale.
// один поток ОС на соединение — не масштабируется на тысячи соединений// one OS thread per connection — doesn't scale to thousands of connections
std::thread::spawn(|| handle_connection_blocking());
// тысячи Future кооперативно на маленьком пуле потоков// thousands of futures cooperatively on a small thread pool
handle_connection_async().await;
Минимальная иллюстративная структура, реализующая poll напрямую, показывает, что async fn — сахар поверх обычной реализации трейта, а не необъяснимая магия компилятора.A minimal illustrative struct implementing poll directly makes it concrete that async fn is sugar over an ordinary trait implementation, not unexplainable compiler magic.
struct AlwaysReady;
impl std::future::Future for AlwaysReady {
type Output = i32;
fn poll(self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context) -> std::task::Poll<i32> {
std::task::Poll::Ready(42) // сразу готова с первого же опроса// ready right away, on the very first poll
}
}
async fn/async-блок, который создан, но никогда не await'нут (и не опрошен executor'ом иначе), не делает АБСОЛЮТНО НИЧЕГО — компилятор Rust даже выдаёт специальное предупреждение "unused implementer of `Future` that must be used", прямая параллель с ленивостью цепочек итераторов (см. iterators.html), но для async-кода, и частый источник путаницы "почему мой async-код вообще не выполняется" у новичков.An async fn/async block that is created but never awaited (or otherwise polled by an executor) does ABSOLUTELY NOTHING — Rust's compiler even emits a specific "unused implementer of `Future` that must be used" warning for this, a direct parallel to the iterator-adaptor-chain laziness gotcha (see iterators.html) but for async code, and a common source of "why isn't my async code running at all" confusion for newcomers.
Поскольку встроенного runtime нет, смешивание Future/утилит из разных async-runtime (например, специфичный для tokio TCP-тип внутри кода, управляемого другим executor'ом) обычно не работает — экосистема исторически сталкивалась с реальными проблемами "совместимости runtime", и выбор (и придерживание) одного runtime на проект — практическая норма, а не случайность.Because there's no bundled runtime, mixing Futures/utilities from different async runtimes (e.g. a tokio-specific TCP type used inside code driven by a different executor) generally does not work — the ecosystem has had real, historical friction around this exact "runtime compatibility" issue, and choosing (and sticking to) one runtime per project is the practical norm, not an accident.
Методы async fn в трейтах долгое время не поддерживались напрямую в стабильном Rust без вспомогательных крейтов (вроде async-trait) из-за фундаментальных сложностей в том, как компилятор представляет тип результирующей Future для метода трейта — поддержка "async fn в трейтах" менялась от версии к версии Rust, и текущее состояние стоит проверять по актуальной документации, а не считать зафиксированным.async fn methods in traits were, for a long time, not directly supported in stable Rust without helper crates (like async-trait) due to fundamental complications around how the compiler represents the resulting Future type for a trait method — "async fn in traits" support has been an evolving area across Rust editions/versions, and specific current-support details are worth verifying against up-to-date documentation rather than assuming a fixed historical state.