Программирование RustКонкурентность и asyncРазработчик Rust серверных приложений

В практической ситуации async функцию вызвали, но полученный future не передали runtime и не ожидали: когда...

В практической ситуации async-функцию вызвали, но полученный future не передали runtime и не ожидали: когда начнётся тело функции?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Тело async-функции не начинается в момент вызова. Вызов только создаёт объект Future; тело начнёт выполняться при первом опросе future — обычно через await или после передачи задачи в runtime. Если future так и не будет опрошен, тело функции не выполнится.

Исторический контекст

Ленивая модель Future позволяет отделить описание асинхронной операции от её планирования. Благодаря этому runtime сам выбирает, когда опрашивать задачу, а несколько операций можно комбинировать без немедленного создания потока или запуска отдельной системной задачи.

Такой подход экономит ресурсы: создание future обычно не выделяет поток и не начинает сетевую или файловую операцию. Запуск происходит только тогда, когда future включена в цепочку выполнения.

Постановка проблемы

Ошибка возникает, когда разработчик воспринимает вызов async fn как немедленный запуск функции. Созданный, но не ожидаемый future не выполняет побочные эффекты: не отправляет запрос, не записывает данные и не запускает код внутри тела.

Это может привести к незаметной потере операции. Например, функция сформировала future записи в базу, но вызывающий код забыл выполнить await или передать future в spawn; программа продолжит работу, а запись не произойдёт.

Подробное решение

Вызов async fn синхронно вычисляет её аргументы и создаёт future, содержащую состояние будущего выполнения. Само тело функции начинает исполняться только при вызове механизма poll.

await организует такое ожидание: он опрашивает future, выполняет доступную часть её тела и при результате Pending передаёт управление вызывающей async-задаче. После пробуждения future будет опрошена снова. Runtime обычно отвечает за вызов poll, регистрацию Waker и планирование повторной проверки.

Передача future в spawn также не означает, что тело выполняется прямо в момент вызова spawn. Runtime принимает задачу в планировщик и начинает её выполнение при последующем опросе. В отличие от обычного await, spawned-задача может выполняться независимо от текущей async-функции.

async fn load() -> u32 { println!("тело выполняется"); 42 } #[tokio::main] async fn main() { let future = load(); println!("future создана"); let value = future.await; println!("результат: {value}"); }

Сначала создаётся future и выводится сообщение из main. Строка из load появляется только после future.await, то есть после начала опроса future.

Если future нужно запускать независимо, используют задачу runtime, например spawn. Это требует учитывать ограничения runtime: для многопоточного планировщика future часто должна быть Send, а её захваченные данные — иметь подходящее время жизни. Обычный await таких требований для передачи между рабочими потоками сам по себе не добавляет.

Ситуация из практики

Сервис получает HTTP-запрос и вызывает асинхронную функцию сохранения результата. Рассматривались три варианта: просто вызвать функцию, выполнить её через await или передать в spawn.

Простой вызов не подходит: future будет создана, но операция не начнётся. await подходит, если ответ сервиса должен зависеть от успешного сохранения; его недостаток — текущая задача ждёт завершения операции. spawn подходит для независимой фоновой работы, но требует отдельно обрабатывать ошибку задачи, отмену и завершение runtime.

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

Что кандидаты часто упускают

1. Равнозначен ли вызов async-функции запуску отдельной задачи?

Нет. Вызов создаёт future в текущем контексте, но не планирует её независимо. Отдельной задачей она становится только после передачи в механизм runtime, например spawn; при этом появляется самостоятельное планирование, отдельное завершение и обычно отдельный JoinHandle.

2. Выполняется ли код до первого внутреннего await сразу после вызова async-функции?

Нет. Даже код в начале тела не выполняется при создании future. Он выполняется во время первого poll; если до первого внутреннего await есть вычисления или побочные эффекты, они также откладываются до этого момента.

3. Обязательно ли для выполнения future наличие async runtime?

Нет, runtime не является частью самого типа Future. Future можно опрашивать вручную, предоставив корректный Context и Waker, хотя это низкоуровневый и сложный путь. На практике runtime нужен для удобного планирования задач, ожидания ввода-вывода, таймеров и повторного опроса после пробуждения.