Вступление#
Часто в приложениях, которые мы разрабатываем, требуется, чтобы различные части системы обменивались данными: один блок кода что-то измеряет, другой собирает статистику, третий принимает решения.
Хочется, чтобы этот обмен был простым, безопасным и предсказуемым. Для этого в Rust существуют каналы. Они позволяют отделить тех, кто отправляет данные (producers), от того, кто принимает и обрабатывает их (consumer).
В этой главе мы снова поднимемся на борт корабля Vectoria. Капитан Нова и робот RUST-Y окажутся в ситуации, где от правильного выбора канала зависит успех спасательной операции.
Вместе с героями мы разберём:
- чем
std::sync::mpscотличается отtokio::sync::mpsc, - почему блокирующий
recv()может остановить выполнение всей программы, - как на практике использовать модель «много отправителей — один получатель».
mpsc, буферам, отправителям и корректному завершению работы.Пролог. Сигнал#
После прохождения через пространственные врата корабль Vectoria дрейфовал у планеты Лира-Бета.
На низкой орбите находился поврежденный медицинский модуль, подающий слабый аварийный сигнал.
Капитан Нова выделил строку с вызовом recv():
// Поток зависает здесь, если сообщений нет:
let msg = rx.recv().unwrap();— «Вот здесь всё и останавливается. Метод recv() в синхронном канале std::sync::mpsc блокирует текущий поток операционной системы до тех пор, пока не придет сообщение. В простом многопоточном приложении это нормально — один поток засыпает в ожидании, пока другой работает.»
Он переключил схему на общий контур управления кораблем:
— «Но у нас однопоточный асинхронный контекст. Когда ты вызываешь блокирующий recv() в основном потоке воркера, ты говоришь планировщику: “Ничего не делай, пока не ответит навигатор”. И пока он рассчитывает траекторию, весь корабль замирает: мы не можем ни щиты скорректировать, ни принять экстренную команду отмены.»
Доктор Арчи добавил: — «Если внутри такой блокировки зависнет система мониторинга состояния жизнеобеспечения, мы узнаем об аварии слишком поздно. Блокирующий вызов в асинхронном коде превращается в угрозу.»
Часть 2. Асинхронные каналы Tokio#
— «Решение не в том, чтобы отказаться от каналов», — продолжит Нова. — «Нам просто нужен инструмент, созданный специально для асинхронной среды. Такой, который не блокирует поток ОС, а приостанавливает только текущую задачу, уступая время другим.»
Он переписал код RUST-Y с использованием tokio::sync::mpsc:
Почему это сработало?#
// Асинхронное чтение освобождает поток планировщика:
let msg = rx.recv().await;
2
- Неблокирующий
recv().await: В асинхронном канале вызовrecv()возвращаетFuture. Когда мы пишем.awaitна строке 2 , планировщик Tokio видит, что данных в канале еще нет. Он временно «замораживает» текущую задачу и переключает поток ОС на выполнение других задач. Как только данные приходят, планировщик возобновляет выполнение. - Ограниченная емкость (Bounded Channel): Асинхронный канал Tokio создается через
mpsc::channel(capacity). Емкость буфера обязательна! Если отправитель шлет сообщения быстрее, чем получатель успевает их читать, буфер переполнится, и вызовsend().awaitприостановит задачу отправителя. Это называется backpressure (обратное давление) и защищает приложение от утечки памяти.
Часть 3. Модель «Многие отправители — один получатель»#
— «Капитан, а если мне нужно собирать телеметрию сразу с нескольких сенсорных модулей?» — спросил RUST-Y. — «Мне создавать канал для каждого?»
— «Нет», — ответил Зори. — «Канал называется mpsc — Multi-producer Single-consumer. Это значит, что отправителей может быть много, а получатель — всегда один. Мы можем просто клонировать передатчик и раздать его разным задачам.»
RUST-Y обновил код, применив клонирование отправителя:
Важные детали работы с клонами Sender:#
- Метод
tx.clone(): Мы можем создавать сколько угодно копий отправителя и передавать их в асинхронные задачи. - Закрытие канала: Канал закрывается автоматически, когда все экземпляры
Sender(включая самый первый, оригинальный) удаляются из памяти (drop). - Явный
drop(tx)вmain: Обратите внимание на строкуdrop(tx)на строке 3 в основной задаче после запуска рабочих потоков. Если мы забудем удалить оригинальныйtxвmain, то получательReceiverв циклеwhile let Some(...)на строке 4 зависнет навсегда. Канал будет считаться открытым, так как одна копияSenderвсе еще находится на мостике, пусть из нее никто ничего и не отправляет.
Эпилог#
— «Кажется, теперь все сходится!» — обрадовался RUST-Y. — «Мы настроили асинхронный канал с датчиков сближения на мостик, поток не блокируется, а исходный отправитель аккуратно удаляется после запуска.»
— «Отлично», — подытожил капитан Нова. — «Связь установлена. Зори, начинаем маневр сближения с модулем доктора Арчи.»
Корабль Vectoria мягко скользнул вперед, а его асинхронные системы работали слаженно и без блокировок.
Проверь свои знания!#
Пройдите небольшой тест, чтобы закрепить понимание работы асинхронных каналов в Rust и Tokio.


