Вступление#
В прошлой главе (Каналы в Rust при поддержке Tokio) мы разобрались, как работают каналы, и помогли роботу RUST-Y перевести связь корабля Vectoria на асинхронный tokio::sync::mpsc, избавившись от опасных блокировок главного потока.
Теперь перед экипажем стоит критически важная задача: состыковаться с дрейфующим медицинским модулем и спасти находящегося там доктора Арчи. Чтобы сделать это безопасно, бортовым системам нужно обрабатывать множество событий одновременно — принимать телеметрию датчиков, слушать ручные команды капитана Новы с мостика и следить за таймером аварийного отключения.
В этой главе мы поможем RUST-Y построить настоящую асинхронную систему обработки событий на базе Tokio. Мы разберем:
- Как одновременно слушать разные асинхронные источники с помощью макроса
tokio::select!. - Как организовать рассылку уведомлений всем системам сразу с помощью
tokio::sync::broadcast. - Как управлять пулом динамически запускаемых задач и обрабатывать их результаты через
tokio::task::JoinSet.

select!, broadcast и JoinSet.Пролог. Сближение#
Медицинский модуль медленно вращался в холодном свете планеты Лира-Бета. Защитный контур модуля («Страж») перешел в аварийный режим и блокировал любые попытки внешней стыковки, считая их угрозой.
На мостике Vectoria Капитан Нова внимательно следил за показателями сближения.
— «Зори, какая дистанция?» — спросил Нова. — «Триста метров. Сближение стабильное, но если автоматика модуля посчитает наш маневр агрессивным, она задействует магнитные выталкиватели. Мы потеряем контакт навсегда.» — «RUST-Y!» — капитан повернулся к роботу. — «Нам нужен надежный диспетчер событий. Мы должны непрерывно считывать телеметрию сближения, слушать ручные команды отмены от Зори и контролировать таймер безопасности. Если телеметрия покажет перегрузку или придет команда отмены — мы должны среагировать мгновенно.»
RUST-Y бодро защелкал реле: — «Капитан! Я переписал код. Больше никаких блокировок потока! Я использую асинхронные каналы Tokio. Но как мне объединить их все в один цикл обработки?»
Нова улыбнулся и указал на голографический экран:
— «Для этого у нас есть макрос tokio::select!. Он работает как стрелочник на железной дороге — ждет, какая ветка сработает первой, и моментально передает управление туда. Давай посмотрим.»
Часть 1. Мультиплексирование и макрос tokio::select!#
Когда нам нужно ожидать выполнения нескольких асинхронных операций одновременно, мы используем tokio::select!. Этот макрос принимает список выражений (Futures) и выполняет ветку той Future, которая завершилась первой.
Давайте посмотрим на схему, которую спроектировал RUST-Y для управления стыковкой:
Как это устроено под капотом?#
tokio::select! {
Some(cmd) = cmd_rx.recv() => { ... }
Some(tel) = telemetry_rx.recv() => { ... }
_ = heartbeat.tick() => { ... }
}- Ожидание первого события:
tokio::select!опрашивает все указанные ветки. Как только в каналеcmd_rxпоявляется команда, или вtelemetry_rxприходит значение, или срабатывает таймерheartbeat.tick(), макрос выполняет соответствующий блок кода. - Безопасность при отмене (Cancellation Safety): Это ключевое понятие асинхронного Rust. Когда одна из ветвей
tokio::select!завершается, все остальные ветви уничтожаются (dropped). Это означает, что невыбранные Future прекращают свое существование.mpsc::Receiver::recvявляется безопасным к отмене: если мы удалим Future ожидания сообщения, само сообщение в канале не потеряется — оно останется в буфере до следующей попытки чтения.- Внимание: Не все операции безопасны к отмене! Например, чтение из сетевого сокета
tokio::io::AsyncReadExt::readпри отмене может привести к потере части уже прочитанных байт.
- Защита от голодания (Starvation): Если бы
select!всегда проверял ветки по порядку (сверху вниз), то при высокой частоте сообщений в первом канале до второго канала очередь могла бы никогда не дойти. Чтобы этого избежать, Tokio при одновременной готовности нескольких веток выбирает одну из них псевдослучайным образом.
Часть 2. Оповещение всех систем через tokio::sync::broadcast#
Стыковка подходила к финальной фазе. Внезапно датчики зафиксировали вспышку на поверхности планеты — в сторону модуля и Vectoria двигалось облако ионизированной плазмы.
— «Капитан! Нам нужно объявить тревогу и немедленно передать команду подготовки к удару на все модули корабля!» — крикнул Зори. — «RUST-Y, отправь сигнал экстренного старта!» — скомандовал Нова.
Робот засуетился:
— «Но капитан! Наш канал mpsc работает по принципу «один отправитель — один получатель». Если я отправлю команду в mpsc, ее получит либо модуль жизнеобеспечения, либо двигатели, но не оба сразу! Нам нужно разослать сообщение абсолютно всем!»
— «Для этого используй broadcast канал», — спокойно ответил Нова. — «Он работает как радиостанция: один передает, а слушают все, кто настроен на эту волну.»
Особенности канала broadcast:#
// Каждая подписка создает новый приемник:
let mut rx = tx.subscribe();
// При чтении медленный получатель отловит ошибку отставания:
Err(RecvError::Lagged(n)) => {
println!("Пропущено {} сообщений!", n);
}- Схема Multi-producer Multi-consumer (MPMC): Любое сообщение, отправленное через
Sender, клонируется и доставляется всем активнымReceiver. Получатели создаются путем вызова методаtx.subscribe()на строке 4 . - Проблема медленного получателя (Lagging): Поскольку буфер канала имеет фиксированный размер (в нашем примере — 2 сообщения), быстрый отправитель может переполнить его, если один из получателей работает медленно.
- В нашем коде Двигательный отсек имитирует долгую работу через
sleep. Когда буфер переполняется, медленный получатель при очередной попытке чтения получит ошибкуRecvError::Lagged(n)на строке 5 . Это сигнализирует о том, чтоnсообщений были пропущены, так как кольцевой буфер перезаписал старые данные. Получатель должен корректно обработать эту ошибку и продолжить чтение актуальных данных.
Часть 3. Динамические задачи и tokio::task::JoinSet#
Стыковочный шлюз модуля доктора Арчи оказался заблокирован обломком обшивки. Нам нужно запустить несколько автономных дронов-зондов, чтобы они обследовали соседние сектора и нашли спасательный челнок доктора, на котором он мог катапультироваться.
Количество зондов может меняться в зависимости от плотности космического мусора.
— «Если мы запустим зонды и будем ждать их через join_all, нам придется ждать завершения работы последнего, даже если первый найдет челнок за секунду», — заметил Зори.
— «Верно», — согласился Нова. — «Нам нужен инструмент, который позволит собирать результаты по мере их готовности. И как только цель будет найдена — мы должны немедленно отозвать (отменить) все остальные зонды, чтобы сберечь энергию.»
RUST-Y открыл терминал:
— «Я знаю! Мы используем tokio::task::JoinSet!»
Почему JoinSet — это отличное решение?#
// Запускаем асинхронную задачу в пул JoinSet:
set.spawn(async move { ... });
// Получаем первый готовый результат:
if let Some(res) = set.join_next().await {
// Отменяем все остальные задачи:
set.abort_all();
}- Динамический пул задач: Мы можем добавлять новые задачи (
Future) прямо во время работы пула с помощьюset.spawn(). - Получение результатов по готовности: Метод
set.join_next().awaitвозвращает результат первой завершившейся задачи. Нам не нужно ждать все остальные. - Мгновенная отмена через
abort_all(): Если мы нашли то, что искали, вызовset.abort_all()отправляет сигнал отмены (cancellation) всем оставшимся задачам. Это изящно решает проблему утечки ресурсов и лишней работы.
Эпилог. Доктор на борту#
Зонд №2 прислал координаты спасательного челнока. RUST-Y моментально отменил остальные задачи, а гравитационный луч Vectoria бережно притянул челнок к основному шлюзу.
Через несколько минут доктор Арчи уже пил горячий чай в кают-компании, а Зори и RUST-Y обсуждали новую асинхронную шину событий корабля.
— «Значит так», — подытожил RUST-Y. — «Если нужно объединить несколько разных источников в один поток — используем tokio::select!. Если нужно разослать сообщение всем системам корабля — берем broadcast. А если запускаем кучу однотипных зондов и хотим гибко ими управлять — нет ничего лучше JoinSet.»
Нова одобрительно кивнул: — «Именно так. Главное — понимать сильные стороны каждого инструмента и не забывать про безопасность отмены.»
Проверь свои знания!#
Давайте проверим, насколько хорошо вы разобрались с отправкой и приемом множества событий в Tokio.


