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

select! и отмена в tokio

select! и отмена

Тема, где начинается настоящая асинхронная инженерия. Спрашивают, что делает 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, остальные разбираются в тренажёре.

  1. #rs_select_cancel1 / 5
    Что означает cancel safety применительно к ветке select!?
    A)Отмена ветки освобождает всю занятую ей память
    B)Ветка не может быть отменена, пока не завершится полностью
    C)Ветка перезапускается с начала при следующей итерации цикла
    D)Прерывание на await не теряет данные и не рвёт инвариант
    показать ответ и разбор
    +D)Прерывание на await не теряет данные и не рвёт инвариант

    // разбор: Проигравшая ветка уничтожается посреди работы. Если она успела прочитать половину сообщения из сокета и не сохранила состояние, байты потеряны — операция не cancel safe. В документации tokio это указано у каждого метода: recv у канала безопасен, а составная операция «прочитать длину, затем тело» — нет. Спасение — обернуть такую операцию в spawn или tokio::pin и не ронять future.

  2. #rs_select_cancel2 / 5
    Как сделать таймаут на асинхронную операцию?
    A)Запустить операцию в spawn и вызвать abort через нужное время из другого потока
    B)Обернуть вызов в std::thread::spawn с ожиданием join_timeout
    C)Поставить лимит на рантайме: он оборвёт все долгие задачи сам
    D)tokio::time::timeout вокруг future — под капотом та же гонка со сном
    показать ответ и разбор
    +D)tokio::time::timeout вокруг future — под капотом та же гонка со сном

    // разбор: timeout — это select! между операцией и таймером, завёрнутый в удобный API: результат приходит как Result, где Err означает истёкшее время. Операция при этом отменяется, поэтому её нужно проверять на cancel safety. Общего ограничителя на длительность задач у рантайма нет.

  3. #rs_select_cancel3 / 5
    Как корректно останавливать фоновые задачи при выключении сервиса?
    A)Дождаться естественного завершения: рантайм не выйдет, пока есть задачи
    B)Раздать токен отмены и ждать завершения задач с общим дедлайном
    C)Завершить процесс: деструкторы всё равно отработают при выходе
    D)Вызвать abort у всех хендлов и сразу выйти из main
    показать ответ и разбор
    +B)Раздать токен отмены и ждать завершения задач с общим дедлайном

    // разбор: Мягкая остановка: задачи слушают CancellationToken или канал остановки в select! рядом с основной работой, доводят текущий запрос до конца и выходят. Дальше — ожидание с дедлайном и только потом принудительное завершение. Резкий abort рвёт операции посреди работы, а выход из main убивает рантайм вместе с недоделанными задачами.

  4. #rs_select_cancel4 / 5
    Почему в цикле с select! опасно создавать future прямо в ветке?
    A)На каждой итерации он создаётся заново, прогресс теряется
    B)Рантайм посчитает такие future утечкой и завершит задачу
    C)Компилятор запрещает создавать future внутри цикла без Box
    D)Ветка станет приоритетной и заблокирует остальные
    показать ответ и разбор
    +A)На каждой итерации он создаётся заново, прогресс теряется

    // разбор: Проигравшая ветка уничтожается, и если future собирается тут же в цикле, следующая итерация начинает с нуля — операция может не завершиться никогда. Поэтому долгоживущие future выносят за цикл и закрепляют через tokio::pin!, а в ветке используют &mut на них.

  5. #rs_select_cancel5 / 5
    Что происходит с проигравшими ветками select! после того, как одна из них завершилась?
    A)Они дорабатывают в фоне, а их результаты рантайм молча отбрасывает как ненужные
    B)Они переносятся в отдельные задачи, чтобы не потерять уже начатую работу
    C)Они ставятся в очередь и будут опрошены на следующей итерации внешнего цикла
    D)Их future уничтожаются прямо в точке ожидания — то есть операции отменяются
    показать ответ и разбор
    +D)Их future уничтожаются прямо в точке ожидания — то есть операции отменяются

    // разбор: select! опрашивает ветки в одной задаче, поэтому проигравшие просто уничтожаются вместе с конструкцией — фоновой доработки нет. Это и есть отмена в асинхронном Rust: операция останавливается в точке ожидания. Если работу терять нельзя, ветку заранее оформляют задачей через spawn — тогда она продолжится независимо от исхода гонки.

дальше

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

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