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

Задачи в tokio: spawn и join

Задачи: spawn, join!, JoinSet

Практический блок: как получить настоящую конкурентность. Спрашивают разницу между двумя await подряд, join! и spawn, и что произойдёт, если задача паникует.

Стержень: await исполняет в текущей задаче, join! опрашивает несколько future конкурентно, spawn отдаёт задачу рантайму.

// Формулировки: «чем spawn отличается от await?», «join! или два spawn?», «что будет при панике в задаче?»

Три способа и их тайминги

Замер на tokio с двумя операциями по 50 мс: два await подряд дают 102 мс, join! - 51 мс, два spawn - 51 мс. Разница принципиальная. await исполняет future прямо здесь, по очереди. join! опрашивает оба конкурентно в рамках одной задачи. spawn отдаёт каждую задачу рантайму, и они могут пойти на разных воркерах.

Отсюда правило выбора: join! проще и не требует Send с 'static, но всё живёт в одной задаче. spawn нужен, когда работа должна пережить текущую функцию или реально разъехаться по ядрам.

// Частая ошибка в обзорах кода - цепочка await там, где запросы независимы. Три вызова по 50 мс превращаются в 150 мс вместо 50.

let (a, b) = tokio::join!(f1(), f2()); // 50 мс
let a = f1().await; let b = f2().await; // 100 мс
join!
конкурентный опрос нескольких future в одной задаче
spawn
передача задачи рантайму; требует 'static и Send

Паника, отмена и группы

Паника в задаче не роняет рантайм: она разворачивает стек внутри задачи, а JoinHandle отдаёт Err с признаком паники. Поэтому у фоновых задач результат стоит проверять, иначе падения будут теряться молча.

abort() отменяет задачу кооперативно: она остановится на ближайшей точке ожидания, а её значения уничтожатся по обычным правилам. Счётный цикл без await так не прервать. Ожидающий увидит JoinError с флагом is_cancelled.

// Для динамической группы задач есть JoinSet: join_next отдаёт результаты по мере готовности, а уничтожение набора отменяет всё, что в нём осталось. Это удобнее вектора хендлов, где приходится ждать в фиксированном порядке.

JoinError
результат неуспешного завершения: паника или отмена
JoinSet
динамическая группа задач с выдачей по готовности

Как отвечать: «Чем tokio::spawn отличается от join!?»

join! опрашивает несколько future конкурентно внутри текущей задачи: они чередуются на одном воркере, требований Send и 'static нет, и всё живёт ровно столько, сколько живёт функция. spawn отдаёт future рантайму как отдельную задачу - она может уехать на другой воркер и пережить вызывающего, поэтому требует Send и 'static. По времени на двух независимых операциях оба варианта дают выигрыш относительно последовательных await. Я беру join!, когда просто нужно не ждать по очереди, и spawn - когда задача должна работать независимо или реально распараллелиться по ядрам.

Ты объясняешь разницу через требования типов и время жизни, а не только через «параллельно или нет» это и есть суть.

На чём валятся

  • Пишут независимые запросы цепочкой await и утраивают время ответа.
  • Считают задачу потоком и ждут от неё вытеснения.
  • Не проверяют результат spawn и не замечают паникующие фоновые задачи.
  • Ждут, что abort прервёт счётный цикл без точек ожидания.
  • Собирают вектор хендлов и ждут их по порядку вместо JoinSet.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.

  1. #rs_tasks1 / 5
    Чем join! отличается от двух последовательных await?
    A)Он запускает каждый future в своём потоке
    B)Он берёт результат того, кто завершился первым
    C)Он опрашивает оба future конкурентно в одной задаче
    D)Он выполняет их последовательно, но без накладных расходов
    показать ответ и разбор
    +C)Он опрашивает оба future конкурентно в одной задаче

    // разбор: Два await подряд — это строгая очередь: второй запрос уйдёт только после ответа на первый. join! опрашивает оба future в рамках одной задачи и ждёт, пока готовы оба: два запроса по 50 мс укладываются в те же 50 мс. Отдельных потоков и задач при этом не создаётся — конкурентность есть, параллелизма нет.

  2. #rs_tasks2 / 5
    Задача, запущенная через spawn, паникует. Что произойдёт с сервисом?
    A)Паника поднимется в задачу, вызвавшую spawn
    B)Рантайм остановится и завершит все остальные задачи
    C)Задача автоматически перезапустится с начала
    D)Рантайм продолжит работу, JoinHandle вернёт Err
    показать ответ и разбор
    +D)Рантайм продолжит работу, JoinHandle вернёт Err

    // разбор: Паника разворачивает стек внутри своей задачи и не переходит границу: рантайм жив, остальные задачи работают, а результат приходит как JoinError с флагом паники. Отсюда практика: у фоновых задач проверять результат join, иначе падения будут теряться молча.

  3. #rs_tasks3 / 5
    Что делает JoinHandle::abort()?
    A)Помечает задачу отменённой: она не проснётся после await
    B)Ставит задачу на паузу до следующего вызова resume
    C)Немедленно прерывает выполнение задачи в текущей точке
    D)Ждёт завершения задачи и возвращает её результат
    показать ответ и разбор
    +A)Помечает задачу отменённой: она не проснётся после await

    // разбор: Отмена в Rust кооперативная: задача останавливается в точке ожидания и дальше не продолжается, а её значения уничтожаются по обычным правилам. Прервать счётный цикл посреди работы abort не может — до ближайшего await задача выполняется как ни в чём не бывало. Ожидающий видит JoinError с is_cancelled.

  4. #rs_tasks4 / 5
    Нужно запустить 1000 задач и обработать результаты по мере готовности. Что взять?
    A)join! на все 1000 future разом
    B)JoinSet: отдаёт результаты в порядке завершения
    C)select! в цикле по всем хендлам
    D)Вектор JoinHandle и await по очереди в цикле
    показать ответ и разбор
    +B)JoinSet: отдаёт результаты в порядке завершения

    // разбор: JoinSet ведёт динамическую группу задач: join_next отдаёт результаты по мере готовности и умеет отменять всю группу при уничтожении. Обход вектора хендлов подряд упирается в самый медленный элемент на каждом шаге, join! требует фиксированного набора на этапе компиляции, а select! в цикле придётся собирать вручную.

  5. #rs_tasks5 / 5
    Что вернёт tokio::spawn и когда начнёт выполняться задача?
    A)JoinHandle, а задача попадёт в очередь планировщика сразу и может стартовать до await
    B)JoinHandle, а задача начнёт выполняться, когда её опросят через await у хендла
    C)Future, который надо дождаться через await, иначе задача не запустится
    D)Идентификатор задачи, по которому её результат забирают из рантайма некоторое время спустя
    показать ответ и разбор
    +A)JoinHandle, а задача попадёт в очередь планировщика сразу и может стартовать до await

    // разбор: spawn отдаёт задачу планировщику немедленно — в отличие от простого вызова async fn, который ничего не запускает. Возвращённый JoinHandle нужен, чтобы забрать результат или отменить задачу, но задача живёт и без него: если хендл уронить, работа продолжится. Это ключевое отличие от ленивых future и главный способ получить настоящую конкурентность.

дальше

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

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