Future и поллинг в Rust
Асинхронность в 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, остальные разбираются в тренажёре.
- Что возвращает метод poll у Future?A)Результат немедленно, блокируя поток до готовностиB)Poll::Ready со значением или Poll::Pending, если не готовоC)Future следующего шага — цепочка строится рекурсивноD)Option: Some при готовности, None пока задача выполняется
показать ответ и разбор
+B)Poll::Ready со значением или Poll::Pending, если не готово// разбор: Модель опроса: исполнитель зовёт poll, задача либо отдаёт готовый результат, либо говорит Pending и запоминает waker. Когда источник события готов (сокет, таймер), он дёргает waker, и задача снова попадает в очередь. Никакого ожидания в самом poll быть не должно — иначе воркер встанет.
- Задача вернула Pending, но waker не был сохранён. Что будет?A)Исполнитель опросит её снова в следующем цикле планировщикаB)Рантайм завершит задачу с ошибкой по таймаутуC)Задача перейдёт в отдельный поток и продолжит выполнение тамD)Задача больше не проснётся — исполнителю нечем её разбудить
показать ответ и разбор
+D)Задача больше не проснётся — исполнителю нечем её разбудить// разбор: Опроса по кругу нет: это была бы активная прокрутка. Исполнитель кладёт задачу в сторону и ждёт сигнала от waker'а. Не сохранил его — задача повисла навсегда, и это типичная ошибка при ручной реализации Future. Готовые примитивы рантайма делают это за автора, поэтому свои future пишут редко.
- Чем асинхронная задача выгоднее потока для ожидания ввода-вывода?A)Она выполняется быстрее за счёт приоритета планировщикаB)Она автоматически распараллеливает вычисления по ядрамC)Она не занимает поток на ожидании и весит сотни байтD)Она не требует синхронизации при доступе к общим данным
показать ответ и разбор
+C)Она не занимает поток на ожидании и весит сотни байт// разбор: Пока задача ждёт сокет, воркер занят другими задачами — состояние ожидающей задачи это лишь её структура-автомат, а не мегабайты стека. Отсюда десятки тысяч соединений на нескольких потоках. На счётной работе выигрыша нет: там задача занимает воркер целиком, и нужен пул потоков.
- Что такое future в Rust?A)Обещание значения, которое рантайм вычисляет в фоне сразу после создания объектаB)Обёртка над потоком операционной системы, скрывающая его создание от программистаC)Дескриптор задачи в очереди планировщика, по которому забирают готовый результатD)Значение, которое умеет продвигаться по шагам, когда его опрашивают
показать ответ и разбор
+D)Значение, которое умеет продвигаться по шагам, когда его опрашивают// разбор: Future — это структура-автомат с методом poll: при каждом опросе она продвигается до следующей точки ожидания и возвращает либо готовый результат, либо Pending. Никакой фоновой работы сама по себе она не делает — в этом ключевое отличие от промисов в других языках, где вычисление стартует в момент создания.
- Зачем future нужен waker?A)Чтобы измерять, сколько времени задача провела в ожидании готовности своего ресурсаB)Чтобы прервать задачу, если она ждёт дольше установленного рантаймом лимитаC)Чтобы источник события мог сообщить исполнителю: эту задачу пора опросить сноваD)Чтобы задача могла переехать на другой воркер, когда текущий перегружен работой
показать ответ и разбор
+C)Чтобы источник события мог сообщить исполнителю: эту задачу пора опросить снова// разбор: Вернув Pending, задача обязана оставить waker там, где произойдёт событие — в реакторе на дескрипторе сокета, в таймере, в очереди канала. Когда событие случилось, вызов wake ставит задачу в очередь готовых. Без этого механизма исполнителю пришлось бы опрашивать все задачи по кругу, тратя процессор впустую.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.