select! и отмена в tokio
Тема, где начинается настоящая асинхронная инженерия. Спрашивают, что делает select! с проигравшими ветками, что такое cancel safety и как правильно останавливать сервис.
Стержень: отмена в Rust это уничтожение future в точке ожидания, поэтому важно, безопасно ли прерывать операцию именно там.
// Формулировки: «что происходит с проигравшей веткой?», «что такое cancel safety?», «как сделать graceful shutdown?»
Гонка и уничтожение проигравших
select! опрашивает ветки и берёт первую готовую, а остальные future просто уничтожает. Классическое применение - таймаут: гонка операции с таймером. Именно на этом построен tokio::time::timeout, который возвращает Err при истечении времени.
Уничтожение проигравшей ветки это и есть отмена. Задача не получает сигнала и не завершает работу аккуратно: её состояние просто дропается там, где она остановилась.
// Проверено: в select! с операцией на 50 мс и сном на 500 мс выигрывает операция, а весь блок занимает те же 50 мс.
tokio::select! {
v = work() => handle(v),
_ = sleep(limit) => timeout(),
}- select!
- гонка нескольких future: побеждает первый готовый
- timeout
- обёртка-гонка операции с таймером
Cancel safety
Раз проигравшая ветка уничтожается посреди работы, возникает вопрос: не потеряется ли что-нибудь. Операция называется cancel safe, если прерывание на await не рвёт инвариант и не теряет данные. У tokio это документировано у каждого метода: recv у канала безопасен, а составное «прочитать длину, затем тело» - нет: половина сообщения уже вычитана из сокета и пропала.
Вторая ловушка того же класса - future, созданный прямо в ветке цикла. На каждой итерации он строится заново, и накопленный прогресс теряется; операция может не завершиться никогда. Долгоживущие future выносят за цикл и закрепляют через tokio::pin!, а в ветке используют &mut на них.
// Практическое правило: прежде чем ставить операцию в select!, посмотри в документацию - раздел Cancel safety там есть не просто так.
- cancel safety
- свойство не терять данные при отмене на await
- tokio::pin!
- закрепление future на месте для переиспользования в цикле
Мягкая остановка
Останавливать сервис через abort всем задачам - плохая идея: запросы оборвутся на середине, транзакции останутся незакоммиченными. Правильная схема - токен отмены: задачи слушают его в select! рядом с основной работой, доводят текущую единицу работы до конца и выходят.
Дальше главный поток ждёт завершения с дедлайном и только по его истечении применяет силу. Обычно это соединяют с обработкой SIGTERM: получили сигнал - перестали принимать новые запросы, дали текущим доработать, вышли.
// Выход из main убивает рантайм вместе с недоделанными задачами, поэтому просто «завершить процесс» - не вариант для сервиса с записями в базу.
- CancellationToken
- общий сигнал остановки для группы задач
- graceful shutdown
- остановка с доведением текущей работы до конца
Как отвечать: «Что такое cancel safety и почему это важно?»
Когда в select! побеждает одна ветка, остальные future уничтожаются прямо в точке ожидания это и есть отмена. Операция cancel safe, если такое прерывание не теряет данные и не рвёт инвариант. Например, recv у канала безопасен: сообщение либо взято, либо нет. А составная операция «прочитать длину, потом тело» небезопасна - если отмена случилась между шагами, часть данных уже вычитана из сокета и потеряна. Поэтому перед тем как ставить операцию в select!, я смотрю раздел Cancel safety в документации, а операции, которые прерывать нельзя, выношу в отдельную задачу через spawn.
Это вопрос, на котором отсеиваются те, кто писал async только по учебнику. Конкретный пример с чтением из сокета показывает, что ты понимаешь механику, а не термин.
На чём валятся
- −Считают отмену вежливым сигналом задаче - на деле её future просто уничтожается.
- −Ставят в select! операцию, небезопасную к отмене, и теряют часть данных.
- −Создают future внутри ветки цикла, и прогресс сбрасывается на каждой итерации.
- −Останавливают сервис через abort всем задачам и обрывают запросы.
- −Не знают про tokio::pin! и не могут переиспользовать future между итерациями.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Что означает cancel safety применительно к ветке select!?A)Отмена ветки освобождает всю занятую ей памятьB)Ветка не может быть отменена, пока не завершится полностьюC)Ветка перезапускается с начала при следующей итерации циклаD)Прерывание на await не теряет данные и не рвёт инвариант
показать ответ и разбор
+D)Прерывание на await не теряет данные и не рвёт инвариант// разбор: Проигравшая ветка уничтожается посреди работы. Если она успела прочитать половину сообщения из сокета и не сохранила состояние, байты потеряны — операция не cancel safe. В документации tokio это указано у каждого метода: recv у канала безопасен, а составная операция «прочитать длину, затем тело» — нет. Спасение — обернуть такую операцию в spawn или tokio::pin и не ронять future.
- Как сделать таймаут на асинхронную операцию?A)Запустить операцию в spawn и вызвать abort через нужное время из другого потокаB)Обернуть вызов в std::thread::spawn с ожиданием join_timeoutC)Поставить лимит на рантайме: он оборвёт все долгие задачи самD)tokio::time::timeout вокруг future — под капотом та же гонка со сном
показать ответ и разбор
+D)tokio::time::timeout вокруг future — под капотом та же гонка со сном// разбор: timeout — это select! между операцией и таймером, завёрнутый в удобный API: результат приходит как Result, где Err означает истёкшее время. Операция при этом отменяется, поэтому её нужно проверять на cancel safety. Общего ограничителя на длительность задач у рантайма нет.
- Как корректно останавливать фоновые задачи при выключении сервиса?A)Дождаться естественного завершения: рантайм не выйдет, пока есть задачиB)Раздать токен отмены и ждать завершения задач с общим дедлайномC)Завершить процесс: деструкторы всё равно отработают при выходеD)Вызвать abort у всех хендлов и сразу выйти из main
показать ответ и разбор
+B)Раздать токен отмены и ждать завершения задач с общим дедлайном// разбор: Мягкая остановка: задачи слушают CancellationToken или канал остановки в select! рядом с основной работой, доводят текущий запрос до конца и выходят. Дальше — ожидание с дедлайном и только потом принудительное завершение. Резкий abort рвёт операции посреди работы, а выход из main убивает рантайм вместе с недоделанными задачами.
- Почему в цикле с select! опасно создавать future прямо в ветке?A)На каждой итерации он создаётся заново, прогресс теряетсяB)Рантайм посчитает такие future утечкой и завершит задачуC)Компилятор запрещает создавать future внутри цикла без BoxD)Ветка станет приоритетной и заблокирует остальные
показать ответ и разбор
+A)На каждой итерации он создаётся заново, прогресс теряется// разбор: Проигравшая ветка уничтожается, и если future собирается тут же в цикле, следующая итерация начинает с нуля — операция может не завершиться никогда. Поэтому долгоживущие future выносят за цикл и закрепляют через tokio::pin!, а в ветке используют &mut на них.
- Что происходит с проигравшими ветками select! после того, как одна из них завершилась?A)Они дорабатывают в фоне, а их результаты рантайм молча отбрасывает как ненужныеB)Они переносятся в отдельные задачи, чтобы не потерять уже начатую работуC)Они ставятся в очередь и будут опрошены на следующей итерации внешнего циклаD)Их future уничтожаются прямо в точке ожидания — то есть операции отменяются
показать ответ и разбор
+D)Их future уничтожаются прямо в точке ожидания — то есть операции отменяются// разбор: select! опрашивает ветки в одной задаче, поэтому проигравшие просто уничтожаются вместе с конструкцией — фоновой доработки нет. Это и есть отмена в асинхронном Rust: операция останавливается в точке ожидания. Если работу терять нельзя, ветку заранее оформляют задачей через spawn — тогда она продолжится независимо от исхода гонки.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.