Перейти к основному содержимому
  1. Rust/

Rust async и Tokio: В ожидании будущего

1776 слов·9 минут· loading · loading · ·
Rust Dev
Оглавление
about-rust - Эта статья часть цикла.
Часть 22: Эта статья

Вступление
#

Иногда программа “замирает” в ожидании задач. Язык Rust, чтобы справиться с ними и избежать простоя, для нас приготовил три разные типа исполнения: синхронность, асинхронность и параллельность. Чтобы разобраться с ними, мы снова отправимся на борт «Vectoria», где находчивый капитан Нова и робот RUST-Y учатся работать с Tokio - рантаймом, который умеет переключать тысячи лёгких задач без простоя.

В конце вас ждёт мини-квиз по Tokio и async: 18 вопросов (разбитые на группы по 8 вопросов) - от основ до практики с кодом по темам: join!, join_all, spawn, JoinSet и другим которые вы встретите в этой статье. Поэтому совет - читайте внимательно, это пригодится при прохождении квиза.

Готовы? Открываем новую главу.


Пролог. Врата
#

Корабль “Vectoria” медленно дрейфовал у древних космических врат. Белёсые дуги уходили в пустоту, будто застывшие волны. На мостике царила напряжённая тишина.

RUST-Y, бортовой робот, переминался на месте: его сенсоры мигали тревожно.
Наконец он собрался с духом и заговорил, голос дрогнул:

— “Капитан”, разрешите обратиться и задать вопрос… Я не понимаю разницы между синхронным и асинхронным кодом. И ещё - все упоминают Tokio. Что это? И чем же асинхронность отличается от параллельности?

Экипаж оживился. Такие вопросы всегда означали, что сейчас будет короткое введени в новую тему - и обычно за этим следовало приключение.

Капитан Нова активировал голографический проектор. В воздухе появились три сцены:

  • Синхронный вызов: команда корабля отправляет запрос на сервер и застывает в ожидании ответа. Экипаж ждёт, пока сервер думает и ответит.
  • Асинхронный вызов: запрос ушёл — но команда продолжает жить своей жизнью: прокладывает курс, регулирует питание, сканирует окрестности. Когда сервер ответит, задача “сообщит о готовности”.
  • Параллельность: несколько членов экипажа работают одновременно - Зори у навигации, Хекс у связи, Арчи в лазарете. Это потоки ОС: тяжёлые, у каждого свой стек и ресурсы.
Sync vs async
Tokio — это диспетчер. Он управляет тысячами лёгких задач (Future) поверх ограниченного числа потоков ОС. Каждая задача знает, когда уступить выполнение (.await). Именно так программа остаётся всегда живой и готовой к выполнению работы.

Часть 1. Когда корабль уснул
#

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

— “Капитан! Навигация замерла. Даже связь упала!”

RUST-Y в панике вывел код на экран:

Ошибка: thread::sleep
 1// ?hidden:start
 2fn query_sensor() -> u32 { 42 }
 3fn process(d: u32) -> u32 { d }
 4
 5use std::{thread, time::Duration};
 6// ?hidden:end
 7fn calibrate() -> u32 {
 8    let d = query_sensor();
 9    thread::sleep(Duration::from_secs(5)); 
10    process(d)
11}
12
13fn main() {
14    println!("[МОСТИК] Запуск калибровки...");
15    let data = calibrate();
16    println!("[МОСТИК] Калибровка завершена: {data}");
17}
18

Проблема:

  • thread::sleep блокирует ОС-поток целиком.
  • Если так “усыпить” воркер рантайма, другие async-задачи не выполнятся.

Нова нахмурился:

— “Ты заставил весь корабль спать вместе с датчиком. thread::sleep блокирует поток. Это словно дежурный задремал на вахте. Пока он спит — системы корабля парализованы”.

Доктор Арчи добавил:

— “Это худшая сторона синхронного кода. В боевых условиях такие паузы смертельно опасны”.


Часть 2. Потоки и их цена
#

Инженер Спаркс пододвинулся к консоли:

— “Предлагаю вынести тяжёлую работу в отдельный поток. Пусть остальное живёт!”

На голограмме вспыхнул новый фрагмент:

Решение: spawn_blocking
 1// ?hidden:start
 2use std::time::Duration;
 3use std::thread;
 4use tokio::task;
 5
 6fn query_sensor() -> u32 { 42 }
 7fn process(d: u32) -> u32 { d }
 8
 9fn calibrate_blocking() -> u32 {
10    let d = query_sensor();
11    thread::sleep(Duration::from_secs(5)); // блокируем, но в отдельном потоке
12    process(d)
13}
14
15// ?hidden:end
16
17#[tokio::main]
18async fn main() {
19    println!("[МОСТИК] Запуск калибровки (spawn_blocking)...");
20    let data = task::spawn_blocking(|| calibrate_blocking())
21        .await
22        .expect("join error");
23    println!("[МОСТИК] Калибровка завершена: {data}");
24}
25

Комментарий

  • spawn_blocking уводит блокирующую работу в пул блокирующих потоков.
  • Это спасает рантайм, но каждый поток — требует дополнительную память; используйте осторожно.

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

Но капитан Нова покачал головой:

— “Каждый поток операционной системы забирает мегабайты памяти. Это временное средство. Мы не можем плодить их тысячами”.


Часть 3. Искусство уступать
#

Через час Vectoria готовилась к гиперпрыжку. Сенсоры снова были перегружены.
Зори докладывал:

— “Если будем ждать по пять секунд синхронно, нас размажет о врата!”

Капитан спокойно включил новую голограмму:

Асинхронная калибровка
 1use tokio::time::{sleep, Duration};
 2
 3async fn query_sensor_async() -> u32 {
 4    sleep(Duration::from_millis(50)).await;
 5    return 42
 6}
 7
 8async fn process_async(d: u32) -> u32 { d }
 9
10async fn calibrate_async() -> u32 {
11    let d = query_sensor_async().await;
12    sleep(Duration::from_secs(1)).await; // уступаем планировщику
13    process_async(d).await
14}
15
16#[tokio::main]
17async fn main() {
18    println!("[МОСТИК] Запуск калибровки (async)...");
19    let data = calibrate_async().await;
20    println!("[МОСТИК] Калибровка завершена: {data}");
21}
22
Комментарий: Асинхронные точки ожидания .await — место где задача уступает поток выполнения рантайму (cooperative).

— “Вот это правильный путь. Мы ждём, но не блокируем. Сенсор думает, а корабль продолжает подготовку. Это и есть сила асинхронности”.


Часть 4. Задачи и обещания
#

RUST-Y вновь задал вопрос:

— “А если функция не async? Как вернуть результат?”

Доктор Арчи поправил очки:

— “Тогда мы возвращаем JoinHandle. Это обещание, что задача выполнится”.

JoinHandle
 1use tokio::task::{self, JoinHandle};
 2
 3fn not_an_async_function() -> JoinHandle<()> {
 4    task::spawn(async {
 5        println!("[ЭКИПАЖ] Второе сообщение из async-задачи");
 6    })
 7}
 8
 9#[tokio::main]
10async fn main() {
11    println!("[МОСТИК] Первое сообщение");
12    let _ = not_an_async_function().await;
13}
14
Комментарий: Возвращаем JoinHandle из sync-функции и ждём её выше (await).

Нова добавил:

— “Но помни: если не дождаться .await, результат уйдёт в пустоту”.

Экипаж понял: JoinHandle - это не просто объект, а инструмент управления задачей.


Часть 5. Мост между мирами
#

Корабль принял сигнал бедствия. Нужно было пропустить сигнал через модуль диагностики прямо из потока операционной системы.

Спаркс нахмурился:

— “Наш код асинхронный (async). Как его связать с обычным потоком?”

Капитан показал:

Handle — мост между потоками
 1use tokio::runtime::Handle;
 2
 3fn not_an_async_function(handle: Handle) {
 4    handle.block_on(async {
 5        println!("[ЭКИПАЖ] Второе сообщение через block_on");
 6    })
 7}
 8
 9#[tokio::main]
10async fn main() {
11    println!("[МОСТИК] Первое сообщение");
12    let handle = Handle::current();
13    std::thread::spawn(move || {
14        not_an_async_function(handle);
15    })
16    .join()
17    .unwrap();
18}
19
Мост между потоком операционной системы и Tokio — Handle::block_on.

— “Handle позволяет ОС-потоку вызвать асинхонную-задачу. Но помните: поток тяжёлый, используем только в исключительных случаях”.


Часть 6. Блокировка в сердце шторма
#

Начинался космический шторм, медлить больше было нельзя.

Во время шторма у врат RUST-Y снова вставил thread::sleep в async-код.
Навигация застыла, индикаторы мигнули красным.

— “Опять корабль уснул!” — закричал Зори.

Нова строго посмотрел на код:

Ошибка: sleep внутри async
Шаг 1/2
 1use std::{thread, time::Duration};
 2
 3async fn bad_sleep() {
 4    // Плохо: блокирует воркер рантайма
 5    thread::sleep(Duration::from_millis(200));
 6    println!("Проснулся (но всё это время рантайм простаивал)");
 7}
 8
 9#[tokio::main]
10async fn main() {
11    let t1 = bad_sleep();
12    let t2 = async {
13        for i in 0..5 {
14            println!("Параллельная задача {i}");
15            tokio::task::yield_now().await;
16        }
17    };
18    tokio::join!(t1, t2);
19}
20
Итог: мало шансов увидеть чередование выполнения — поток занят thread::sleep.

Он тут же внес исправление (шаг 2). 👆🏻

Свет тревоги погас, корабль вернулся в строй.


Часть 7. Научиться делиться временем выполнения
#

Команда выполнила тренировочный пробный манёвр и навигация сожрала все ресурсы процессора. Связь и сенсоры замерли.

Нова объяснил:

yield_now — уступаем место
 1use tokio::task;
 2
 3async fn looper(name: &str) {
 4    for i in 0..3 {
 5        println!("{name}-{i}");
 6        task::yield_now().await;
 7    }
 8}
 9
10#[tokio::main]
11async fn main() {
12    tokio::join!(looper("A"), looper("B"));
13}
14
Кооперативность: yield_now явно отдаёт квант выполнения другим задачам.

— “В async Rust Tokio многозадачность кооперативная. Задачи должны уступать сами.
Без .await или yield_now().await — планировщик бессилен”.


Часть 8. Экипаж действует как единое целое
#

Перед прыжком нужно было:

  • проверить реактор,
  • перезапустить связь,
  • откалибровать навигацию.

Капитан показал решение:

Несколько задач: join!
 1use tokio::time::{sleep, Duration};
 2
 3async fn job(id: u32) -> u32 {
 4    sleep(Duration::from_millis(60 * id as u64)).await;
 5    println!("Задача {id} завершена");
 6    id * 10
 7}
 8
 9#[tokio::main]
10async fn main() {
11    let (a, b) = tokio::join!(job(1), job(2));
12    println!("Результаты: {a}, {b}");
13}
14
Для фиксированного набора задач используйте tokio::join!

— “А если задач десятки, сотни?” — спросил Расти.

Коллекция задач: join_all
 1// ?hidden:start
 2use tokio::time::{sleep, Duration};
 3use futures::future::join_all;
 4// ?hidden:end
 5async fn work(i: u32) -> u32 {
 6    sleep(Duration::from_millis(50 * i as u64)).await;
 7    i * 2
 8}
 9
10#[tokio::main]
11async fn main() {
12    let futs = vec![work(1), work(2), work(3)];
13    let results = join_all(futs).await;
14    println!("Результаты: {:?}", results); // [2,4,6]
15}

Используем join_all из пакета futures

  • Он ждёт коллекцию Future и возвращает Vec результатов.
  • tokio::join! — фиксированный набор задач → кортеж.
  • futures::future::join_all — коллекция задач → Vec результатов.

Часть 9. Ремонтные дроны и фоновая работа
#

— “Запустить ремонтных дронов!” — приказал капитан.

RUST-Y замялся:
— “Как запустить задачу в фоне?”

Нова вывел на экран код:

Хекс уточнил:
— “А если дронов много?”

Нова вывел другой код на на экран:

JoinSet: задачи по мере завершения
 1use tokio::task::JoinSet;
 2use tokio::time::{sleep, Duration};
 3
 4async fn unit(i: u32) -> u32 {
 5    sleep(Duration::from_millis(30 * i as u64)).await;
 6    i * 100
 7}
 8
 9#[tokio::main]
10async fn main() {
11    let mut set = JoinSet::new();
12    for i in 1..=5 {
13        set.spawn(unit(i));
14    }
15
16    while let Some(res) = set.join_next().await {
17        match res {
18            Ok(v) => println!("Готов блок: {v}"),
19            Err(e) => eprintln!("Ошибка задачи: {e}"),
20        }
21    }
22    println!("JoinSet завершил все задачи");
23}
24
JoinSet позволяет получать результаты по мере готовности (в любом порядке).

Эпилог. Ожидающее будущее
#

Шторм утих. Сенсоры работали, все подсистемы были в порядке, дроны вернулись.

RUST-Y стоял на мостике:

— “Спасибо, капитан. Теперь я понял: ошибки — это ступени развития. Async и Future — это жизнь без блокировок”.

Нова улыбнулся:

— “Ничего, мы понемногу учимся жить в ритме вселенной. А теперь готовы к новым испытаниям”.

Врата активировались и Vectoria умчалась в даль скозь сияние портала.


Приложение: шпаргалка капитана по ключевым приёмам
#

  • Не блокируйте: thread::sleep → заменяйте на tokio::time::sleep.
  • Тяжёлое CPU/блокирующее I/O: оборачивайте в tokio::task::spawn_blocking.
  • Старт из sync-кода: используйте tokio::runtime::Handle::block_on.
  • Параллельное ожидание: tokio::join! для фиксированных задач; futures::join_all для коллекций.
  • Фоновая работа: tokio::spawn (требует 'static или move).
  • Пачки задач: tokio::task::JoinSet + join_next().await.
  • Кооперативность: .await; при долгих циклах — yield_now().await.

Финальный квиз 🚀
#

🛰️

about-rust - Эта статья часть цикла.
Часть 22: Эта статья

Связанные статьи

Как работает Vec в Rust: устройство, методы, оптимизация
2268 слов·11 минут· loading · loading
Rust Dev
BufReader в Rust: ускоряем ввод-вывод
1861 слово·9 минут· loading · loading
Rust Dev
Поиск данных в файле - Cli хелпер для мастера с помощью Rust
1812 слов·9 минут· loading · loading
Rust Dev