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

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

·1899 слов·9 минут· loading · loading · · ·Rust-middle
Оглавление
О Rust - Эта статья часть цикла.
Статей прочитано 0/38
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
54 Rust async и Tokio: В ожидании будущего (текущая)
Не прочитана

Вступление
#

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

Проблема:

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

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

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

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

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


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

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

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

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

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

Комментарий

  • 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 умчалась в даль скозь сияние портала.


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

Шпаргалка капитана по асинхронности:
#

Чтобы уверенно пройти сквозь врата async-программирования и не завалить финальный тест, держи под рукой этот краткий бортовой журнал:

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

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

🛰️

История обновлений

  • Установил уровень сложности rust-middle для статьи.
Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/38
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
54 Rust async и Tokio: В ожидании будущего (текущая)
Не прочитана

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