Программирование RustКонкурентность и asyncRust-разработчик асинхронных сетевых сервисов

В чём состоит правильный порядок проверки готовности события и регистрации Waker, предотвращающий потерю пр...

В чём состоит правильный порядок проверки готовности события и регистрации Waker, предотвращающий потерю пробуждения в future?

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

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

Future должна зарегистрировать актуальный Waker, а затем повторно проверить состояние события в рамках корректной синхронизации. Если сначала проверить событие, а потом зарегистрировать Waker, между этими действиями событие может стать готовым, но пробуждать будет некого; future останется в состоянии Pending навсегда.

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

Асинхронные runtime строятся вокруг событийных источников: сокетов, таймеров, файловых дескрипторов и пользовательских сигналов готовности. Вместо блокировки задачи источник сообщает runtime, что future можно снова опросить.

Такой подход экономит потоки, но вводит гонку между проверкой состояния ресурса и подпиской на уведомления. Протокол регистрации Waker нужен именно для устранения этой гонки, известной как потеря пробуждения.

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

Предположим, future сначала проверяет, готов ли сокет, и видит отрицательный результат. До регистрации Waker другой поток или обработчик события делает сокет готовым и пытается разбудить future, но зарегистрированного Waker ещё нет.

После этого future регистрирует Waker и возвращает Pending. Новое событие уже произошло, повторного уведомления может не быть, поэтому задача перестанет продвигаться. Это не просто задержка: при отсутствии другого пробуждения она может зависнуть навсегда.

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

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

Если событие уже готово, future возвращает Poll::Ready. Если оно ещё не готово, future возвращает Poll::Pending, оставляя зарегистрированный Waker. Когда производитель обнаруживает готовность, он переводит состояние и вызывает wake для сохранённого Waker.

Критична именно повторная проверка после регистрации. Если событие произошло до регистрации, повторная проверка обнаружит готовое состояние. Если событие произошло после регистрации, производитель увидит Waker и вызовет его. Синхронизация должна исключать промежуточное состояние, в котором событие уже считается обработанным, но Waker ещё не виден производителю.

Вызов wake не выполняет future немедленно и не гарантирует, что она завершится. Он лишь сообщает runtime, что future нужно снова поставить на опрос. При следующем poll future обязана заново проверить состояние ресурса, поскольку уведомления могут объединяться, приходить несколько раз или быть вызваны для уже устаревшего состояния.

На практике Waker обычно заменяют только при необходимости, сравнивая его через will_wake, чтобы не выполнять лишние операции. Вызов wake часто выносят за пределы блокировки: сам Waker может разбудить планировщик и потенциально вызвать сложную внутреннюю логику.

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

Команда реализует future ожидания сообщения от собственного сетевого драйвера. Первый вариант сначала проверяет очередь сообщений, а при её пустоте сохраняет Waker. Под нагрузкой иногда возникают зависшие задачи: сообщение появляется между двумя действиями и не вызывает уведомление.

Рассматривались два решения. Можно было всегда периодически опрашивать очередь по таймеру — это проще, но создаёт лишнюю задержку и нагрузку. Можно было использовать блокировку вокруг регистрации Waker, изменения состояния очереди и проверки готовности — это требует аккуратного протокола, зато не теряет события.

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

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

  1. Почему недостаточно просто зарегистрировать Waker, а затем вернуть Pending?

    Ответ: регистрация Waker не означает, что событие ещё не произошло. Оно могло стать готовым непосредственно перед регистрацией или сразу после неё. Поэтому future должна проверить состояние после регистрации и вернуть Ready, если результат уже доступен. Иначе она может оставить готовое событие без последующего пробуждения.

  2. Гарантирует ли каждый вызов wake отдельный вызов poll?

    Ответ: нет. Runtime может объединить несколько пробуждений, пока задача уже находится в очереди на выполнение. Поэтому future не должна рассчитывать на количество вызовов wake; каждый poll должен самостоятельно проверять актуальное состояние ресурса и обрабатывать все доступные результаты.

  3. Почему нельзя считать Waker заменой синхронизации состояния события?

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