Задачи в tokio: spawn и join
Практический блок: как получить настоящую конкурентность. Спрашивают разницу между двумя 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, остальные разбираются в тренажёре.
- Чем join! отличается от двух последовательных await?A)Он запускает каждый future в своём потокеB)Он берёт результат того, кто завершился первымC)Он опрашивает оба future конкурентно в одной задачеD)Он выполняет их последовательно, но без накладных расходов
показать ответ и разбор
+C)Он опрашивает оба future конкурентно в одной задаче// разбор: Два await подряд — это строгая очередь: второй запрос уйдёт только после ответа на первый. join! опрашивает оба future в рамках одной задачи и ждёт, пока готовы оба: два запроса по 50 мс укладываются в те же 50 мс. Отдельных потоков и задач при этом не создаётся — конкурентность есть, параллелизма нет.
- Задача, запущенная через spawn, паникует. Что произойдёт с сервисом?A)Паника поднимется в задачу, вызвавшую spawnB)Рантайм остановится и завершит все остальные задачиC)Задача автоматически перезапустится с началаD)Рантайм продолжит работу, JoinHandle вернёт Err
показать ответ и разбор
+D)Рантайм продолжит работу, JoinHandle вернёт Err// разбор: Паника разворачивает стек внутри своей задачи и не переходит границу: рантайм жив, остальные задачи работают, а результат приходит как JoinError с флагом паники. Отсюда практика: у фоновых задач проверять результат join, иначе падения будут теряться молча.
- Что делает JoinHandle::abort()?A)Помечает задачу отменённой: она не проснётся после awaitB)Ставит задачу на паузу до следующего вызова resumeC)Немедленно прерывает выполнение задачи в текущей точкеD)Ждёт завершения задачи и возвращает её результат
показать ответ и разбор
+A)Помечает задачу отменённой: она не проснётся после await// разбор: Отмена в Rust кооперативная: задача останавливается в точке ожидания и дальше не продолжается, а её значения уничтожаются по обычным правилам. Прервать счётный цикл посреди работы abort не может — до ближайшего await задача выполняется как ни в чём не бывало. Ожидающий видит JoinError с is_cancelled.
- Нужно запустить 1000 задач и обработать результаты по мере готовности. Что взять?A)join! на все 1000 future разомB)JoinSet: отдаёт результаты в порядке завершенияC)select! в цикле по всем хендламD)Вектор JoinHandle и await по очереди в цикле
показать ответ и разбор
+B)JoinSet: отдаёт результаты в порядке завершения// разбор: JoinSet ведёт динамическую группу задач: join_next отдаёт результаты по мере готовности и умеет отменять всю группу при уничтожении. Обход вектора хендлов подряд упирается в самый медленный элемент на каждом шаге, join! требует фиксированного набора на этапе компиляции, а select! в цикле придётся собирать вручную.
- Что вернёт tokio::spawn и когда начнёт выполняться задача?A)JoinHandle, а задача попадёт в очередь планировщика сразу и может стартовать до awaitB)JoinHandle, а задача начнёт выполняться, когда её опросят через await у хендлаC)Future, который надо дождаться через await, иначе задача не запуститсяD)Идентификатор задачи, по которому её результат забирают из рантайма некоторое время спустя
показать ответ и разбор
+A)JoinHandle, а задача попадёт в очередь планировщика сразу и может стартовать до await// разбор: spawn отдаёт задачу планировщику немедленно — в отличие от простого вызова async fn, который ничего не запускает. Возвращённый JoinHandle нужен, чтобы забрать результат или отменить задачу, но задача живёт и без него: если хендл уронить, работа продолжится. Это ключевое отличие от ленивых future и главный способ получить настоящую конкурентность.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.