сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Async и tokio

Future и поллинг в Rust

Future и модель опроса

Асинхронность в Rust устроена не так, как в JavaScript или Go, и на собесе это проверяют первым делом. Спрашивают, что происходит при вызове async-функции без await, во что компилятор превращает её тело и как задача просыпается.

Стержень: future ленив и ничего не делает, пока его не опросят; движется всё моделью «опрос плюс пробуждение», а не потоками.

// Формулировки: «что будет, если не написать .await?», «во что превращается async fn?», «зачем Waker?»

Ленивость

Вызов async-функции только строит объект-future - по замеру это десятки наносекунд, тело при этом не выполняется вовсе. Отсюда предупреждение must_use на неиспользованном future и типовая ошибка новичка: вызвал функцию, забыл await, ничего не произошло.

Отличие от промисов важное: в JavaScript промис начинает работу сразу при создании, а тут - только когда его опрашивают. Поэтому «запустить в фоне» это не создать future, а отдать его рантайму через spawn.

// Проверено на tokio: создание future занимает наносекунды, а два await подряд по 50 мс дают 100 мс - работа идёт строго по очереди, пока не попросишь иначе.

let f = work();      // ничего не делает
let r = f.await;     // вот теперь работает
ленивость
future не выполняется без опроса
must_use
предупреждение о созданном, но не использованном future

Стейт-машина и поллинг

Компилятор режет тело async-функции по точкам await: каждая становится состоянием конечного автомата, а локальные переменные, живущие через await, переезжают в структуру состояния. Отсюда и размер future - сотня-другая байт для простой функции, и требование Pin: у автомата бывают ссылки на собственные поля.

Исполнитель вызывает poll. Задача либо возвращает Poll::Ready со значением, либо Poll::Pending, и тогда обязана сохранить Waker. Когда источник события готов (сокет, таймер), он дёргает waker, и задача снова попадает в очередь на опрос. Никакого опроса по кругу нет это была бы активная прокрутка.

// Практический вывод: блокирующая операция внутри poll или внутри задачи останавливает весь воркер, потому что управление рантайму не вернулось.

poll
опрос future: Ready со значением или Pending
Waker
механизм пробуждения задачи при готовности события

Как отвечать: «Что произойдёт, если вызвать async-функцию и не написать .await?»

Ничего не выполнится. Вызов только строит объект-future, тело начинает работать, когда его кто-то опрашивает. Компилятор об этом предупредит через must_use, но ошибкой это не будет. Отличие от промисов в JavaScript как раз здесь: там работа стартует при создании, а в Rust future ленив. Если задачу нужно запустить именно в фоне, её отдают рантайму через tokio::spawn - тогда await можно сделать позже или не делать вовсе, а результат забрать через JoinHandle.

Ты объясняешь механику и сразу разводишь её с моделью из другого языка - типичный источник путаницы, который интервьюер и проверяет.

На чём валятся

  • Считают, что async-функция стартует при вызове, как промис.
  • Не могут объяснить, во что компилируется async fn, и говорят про потоки.
  • Пишут блокирующий вызов внутри задачи - воркер встаёт вместе со всеми её соседями.
  • Реализуют Future вручную и забывают сохранить waker - задача больше не просыпается.
  • Думают, что исполнитель опрашивает задачи по кругу.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_future1 / 5
    Что возвращает метод poll у Future?
    A)Результат немедленно, блокируя поток до готовности
    B)Poll::Ready со значением или Poll::Pending, если не готово
    C)Future следующего шага — цепочка строится рекурсивно
    D)Option: Some при готовности, None пока задача выполняется
    показать ответ и разбор
    +B)Poll::Ready со значением или Poll::Pending, если не готово

    // разбор: Модель опроса: исполнитель зовёт poll, задача либо отдаёт готовый результат, либо говорит Pending и запоминает waker. Когда источник события готов (сокет, таймер), он дёргает waker, и задача снова попадает в очередь. Никакого ожидания в самом poll быть не должно — иначе воркер встанет.

  2. #rs_future2 / 5
    Задача вернула Pending, но waker не был сохранён. Что будет?
    A)Исполнитель опросит её снова в следующем цикле планировщика
    B)Рантайм завершит задачу с ошибкой по таймауту
    C)Задача перейдёт в отдельный поток и продолжит выполнение там
    D)Задача больше не проснётся — исполнителю нечем её разбудить
    показать ответ и разбор
    +D)Задача больше не проснётся — исполнителю нечем её разбудить

    // разбор: Опроса по кругу нет: это была бы активная прокрутка. Исполнитель кладёт задачу в сторону и ждёт сигнала от waker'а. Не сохранил его — задача повисла навсегда, и это типичная ошибка при ручной реализации Future. Готовые примитивы рантайма делают это за автора, поэтому свои future пишут редко.

  3. #rs_future3 / 5
    Чем асинхронная задача выгоднее потока для ожидания ввода-вывода?
    A)Она выполняется быстрее за счёт приоритета планировщика
    B)Она автоматически распараллеливает вычисления по ядрам
    C)Она не занимает поток на ожидании и весит сотни байт
    D)Она не требует синхронизации при доступе к общим данным
    показать ответ и разбор
    +C)Она не занимает поток на ожидании и весит сотни байт

    // разбор: Пока задача ждёт сокет, воркер занят другими задачами — состояние ожидающей задачи это лишь её структура-автомат, а не мегабайты стека. Отсюда десятки тысяч соединений на нескольких потоках. На счётной работе выигрыша нет: там задача занимает воркер целиком, и нужен пул потоков.

  4. #rs_future4 / 5
    Что такое future в Rust?
    A)Обещание значения, которое рантайм вычисляет в фоне сразу после создания объекта
    B)Обёртка над потоком операционной системы, скрывающая его создание от программиста
    C)Дескриптор задачи в очереди планировщика, по которому забирают готовый результат
    D)Значение, которое умеет продвигаться по шагам, когда его опрашивают
    показать ответ и разбор
    +D)Значение, которое умеет продвигаться по шагам, когда его опрашивают

    // разбор: Future — это структура-автомат с методом poll: при каждом опросе она продвигается до следующей точки ожидания и возвращает либо готовый результат, либо Pending. Никакой фоновой работы сама по себе она не делает — в этом ключевое отличие от промисов в других языках, где вычисление стартует в момент создания.

  5. #rs_future5 / 5
    Зачем future нужен waker?
    A)Чтобы измерять, сколько времени задача провела в ожидании готовности своего ресурса
    B)Чтобы прервать задачу, если она ждёт дольше установленного рантаймом лимита
    C)Чтобы источник события мог сообщить исполнителю: эту задачу пора опросить снова
    D)Чтобы задача могла переехать на другой воркер, когда текущий перегружен работой
    показать ответ и разбор
    +C)Чтобы источник события мог сообщить исполнителю: эту задачу пора опросить снова

    // разбор: Вернув Pending, задача обязана оставить waker там, где произойдёт событие — в реакторе на дескрипторе сокета, в таймере, в очереди канала. Когда событие случилось, вызов wake ставит задачу в очередь готовых. Без этого механизма исполнителю пришлось бы опрашивать все задачи по кругу, тратя процессор впустую.

дальше

Теорию прочитали. Навык ставится повторением

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