Как async runtime приводит созданный future к выполнению?
Создание future не запускает его тело: future — это ленивое описание асинхронной работы. Async runtime передаёт future исполнителю, который многократно вызывает его метод poll; результат Ready означает завершение, а Pending — необходимость дождаться внешнего события и повторить опрос после уведомления через Waker.
Сначала выводится future создан, затем тело work: вызов work() только создаёт future, а block_on запускает его через runtime.
Асинхронный подход появился как способ обслуживать большое число операций ожидания без выделения отдельного блокирующего потока на каждую операцию. Это особенно важно для сетевых серверов, где значительная часть времени уходит на ожидание сокета, таймера или другого внешнего события.
Модель Future/Executor отделяет описание работы от её исполнения. Future хранит состояние незавершённой операции, а executor решает, когда выделить ей поток и когда возобновить выполнение.
После встречи с точкой ожидания future не должен занимать поток активным циклом проверки. Если он возвращает Pending, runtime должен понимать, какое событие способно продвинуть работу, и получить уведомление, когда это событие произошло.
Неверная реализация приводит к разным последствиям: busy-waiting расходует CPU, отсутствие уведомления оставляет задачу навсегда приостановленной, а блокирующий вызов внутри async-задачи может занять поток executor и задержать другие задачи.
При вызове poll future получает контекст с Waker. Если работа ещё не завершена, future сохраняет нужное внутреннее состояние и возвращает Poll::Pending. Когда операция готова продолжиться, связанный источник события вызывает waker, после чего runtime ставит задачу в очередь повторного опроса.
При следующем poll future продолжает работу с сохранённого состояния, а не начинает async-функцию заново. Компилятор преобразует тело async-функции в состояние, включающее локальные значения, необходимые между точками ожидания.
Runtime обычно управляет не отдельными future напрямую, а задачами, содержащими future. Он выбирает готовые задачи, вызывает их poll на рабочих потоках и возвращает поток в планировщик, если future сообщает Pending. Конкретная политика распределения задач, число потоков и поддержка I/O зависят от runtime.
Важное ограничение: Waker — это не команда немедленно выполнить future на текущем стеке. Это уведомление executor о том, что задачу нужно снова рассмотреть. Повторный вызов poll без реального прогресса нарушает контракт эффективной работы и может вызвать лишнюю нагрузку.
Future нельзя произвольно опрашивать одновременно из нескольких потоков. Runtime обеспечивает корректную последовательность опроса конкретной задачи, а возможность перемещения самой задачи между потоками определяется требованиями Send её состояния. Это отличается от вопроса о том, является ли общий тип Sync.
Асинхронный runtime не делает любой вызов неблокирующим автоматически. Синхронное чтение файла, длительный расчёт или блокирующее ожидание внутри async-задачи всё равно блокируют поток, если их явно не вынести на специальный blocking-пул или в отдельный поток.
Команда подключила к серверу библиотеку внешнего устройства. Библиотека возвращает собственный future: он проверяет очередь событий устройства и должен вызвать сохранённый waker после поступления ответа. В тестах задача иногда зависает навсегда, хотя ответ фактически получен.
Были рассмотрены варианты:
poll. Это требует аккуратно синхронизировать состояние, зато сохраняет событийную модель и не тратит CPU впустую.Выбран третий вариант. Причина зависания была не в самом executor, а в нарушении контракта future: готовность результата не сопровождалась уведомлением. После вызова waker задача стала повторно опрашиваться и завершаться без таймерного polling.
Вопрос: Может ли future начать выполняться сразу после вызова async-функции?
Ответ: Нет, сам вызов async-функции обычно создаёт объект future и выполняет только код, необходимый для его создания. Тело асинхронной операции продвигается вызовами poll, которые инициирует executor или другой future, ожидающий этот объект.
Это объясняет, почему future может быть создан, передан в структуру или коллекцию и никогда не выполнить никакой работы. Чтобы он начал исполняться, его нужно передать runtime либо явно встроить в цепочку, которую уже опрашивают.
Вопрос: Что произойдёт, если future вернул Pending, но его waker не был вызван после готовности события?
Ответ: Runtime обычно не обязан самостоятельно постоянно опрашивать такую задачу. Она останется неактивной, пока другое событие случайно не разбудит её или пока не будет вызван сохранённый waker.
Поэтому корректный future обязан связать waker с конкретным источником прогресса и вызвать его при переходе в состояние, в котором следующий poll может продвинуть или завершить операцию. Само возвращение Pending не является обещанием runtime периодически проверять future.
Вопрос: Почему вызов блокирующей функции внутри async-задачи опасен даже при наличии многопоточного runtime?
Ответ: Async-задача занимает рабочий поток runtime до тех пор, пока её poll не вернётся. Если внутри этого опроса выполняется блокирующая функция, поток не может перейти к другим задачам; при достаточном числе таких вызовов все рабочие потоки могут оказаться заняты.
Возможные решения — использовать неблокирующий async API, вынести операцию в blocking-пул runtime или выполнить её в отдельном потоке. Неблокирующий API обычно лучше масштабируется, а blocking-пул проще применить для существующей синхронной библиотеки, но он требует контроля размера пула, отмены и потребления ресурсов.