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

> История экипажа Vectoria — наглядное объяснение синхронного и асинхронного кода, разницы с параллельностью, и практики Tokio: spawn, spawn_blocking, join!, join_all, JoinHandle, Handle, cooperative multitasking.

- **URL:** https://eagle2000.space/rust/future-async/
- **Date:** 2025-09-18

- **Tags:** rust, dev

---## Вступление

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

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

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

---

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

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

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

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

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

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

- **Синхронный вызов:** команда корабля отправляет запрос на сервер и застывает в ожидании ответа. Экипаж ждёт, пока сервер думает и ответит.
- **Асинхронный вызов:** запрос ушёл — но команда продолжает жить своей жизнью: прокладывает курс, регулирует питание, сканирует окрестности. Когда сервер ответит, задача "сообщит о готовности".  
- **Параллельность:** несколько членов экипажа работают одновременно - Зори у навигации, Хекс у связи, Арчи в лазарете. Это потоки ОС: тяжёлые, у каждого свой стек и ресурсы.  


{{< figure src="Frame 27.jpg" alt="Sync vs async" >}}

{{< alert "comment" >}}
Tokio — это диспетчер. Он управляет тысячами лёгких задач (`Future`) поверх ограниченного числа потоков ОС. Каждая задача знает, когда уступить выполнение (`.await`). Именно так программа остаётся всегда живой и готовой к выполнению работы.
{{< /alert >}}

---

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

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

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

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


### 💻 Ошибка: thread::sleep


```rust
// ?ignore
fn query_sensor() -> u32 { 42 }
fn process(d: u32) -> u32 { d }

use std::{thread, time::Duration};
fn calibrate() -> u32 {
    let d = query_sensor();
    thread::sleep(Duration::from_secs(5)); 
    process(d)
}

fn main() {
    println!("[МОСТИК] Запуск калибровки...");
    let data = calibrate();
    println!("[МОСТИК] Калибровка завершена: {data}");
}
```

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


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

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

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

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


---

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

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

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


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


### 💻 Решение: spawn_blocking


```rust
// ?ignore
use std::time::Duration;
use std::thread;
use tokio::task;

fn query_sensor() -> u32 { 42 }
fn process(d: u32) -> u32 { d }

fn calibrate_blocking() -> u32 {
    let d = query_sensor();
    thread::sleep(Duration::from_secs(5)); // блокируем, но в отдельном потоке
    process(d)
}

#[tokio::main]
async fn main() {
    println!("[МОСТИК] Запуск калибровки (spawn_blocking)...");
    let data = task::spawn_blocking(|| calibrate_blocking())
        .await
        .expect("join error");
    println!("[МОСТИК] Калибровка завершена: {data}");
}
```

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


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

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

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

---


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

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


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

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


### 💻 Асинхронная калибровка


```rust
use tokio::time::{sleep, Duration};

async fn query_sensor_async() -> u32 {
    sleep(Duration::from_millis(50)).await;
    return 42
}

async fn process_async(d: u32) -> u32 { d }

async fn calibrate_async() -> u32 {
    let d = query_sensor_async().await;
    sleep(Duration::from_secs(1)).await; // уступаем планировщику
    process_async(d).await
}

#[tokio::main]
async fn main() {
    println!("[МОСТИК] Запуск калибровки (async)...");
    let data = calibrate_async().await;
    println!("[МОСТИК] Калибровка завершена: {data}");
}
```

Комментарий:
Асинхронные точки ожидания .await — место где задача уступает поток выполнения рантайму (cooperative).


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


---

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

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

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

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

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


### 💻 JoinHandle


```rust
use tokio::task::{self, JoinHandle};

fn not_an_async_function() -> JoinHandle<()> {
    task::spawn(async {
        println!("[ЭКИПАЖ] Второе сообщение из async-задачи");
    })
}

#[tokio::main]
async fn main() {
    println!("[МОСТИК] Первое сообщение");
    let _ = not_an_async_function().await;
}
```

Комментарий:
Возвращаем JoinHandle из sync-функции и ждём её выше (await).


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

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

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

---


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

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

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

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

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


### 💻 Handle — мост между потоками


```rust
use tokio::runtime::Handle;

fn not_an_async_function(handle: Handle) {
    handle.block_on(async {
        println!("[ЭКИПАЖ] Второе сообщение через block_on");
    })
}

#[tokio::main]
async fn main() {
    println!("[МОСТИК] Первое сообщение");
    let handle = Handle::current();
    std::thread::spawn(move || {
        not_an_async_function(handle);
    })
    .join()
    .unwrap();
}
```

Мост между потоком операционной системы и Tokio — Handle::block_on.



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

---

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


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

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


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

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


### 💻 Ошибка: sleep внутри async


#### Шаг 1

```rust
use std::{thread, time::Duration};

async fn bad_sleep() {
    // Плохо: блокирует воркер рантайма
    thread::sleep(Duration::from_millis(200));
    println!("Проснулся (но всё это время рантайм простаивал)");
}

#[tokio::main]
async fn main() {
    let t1 = bad_sleep();
    let t2 = async {
        for i in 0..5 {
            println!("Параллельная задача {i}");
            tokio::task::yield_now().await;
        }
    };
    tokio::join!(t1, t2);
}
```

Итог: мало шансов увидеть чередование выполнения — поток занят thread::sleep.


#### Шаг 2

```rust
use tokio::time::{sleep, Duration};

async fn good_sleep() {
    // Хорошо: уступаем планировщику
    sleep(Duration::from_millis(200)).await;
    println!("Проснулся (планировщик выполнял другие задачи)");
}

#[tokio::main]
async fn main() {
    let t1 = good_sleep();
    let t2 = async {
        for i in 0..5 {
            println!("Параллельная задача {i}");
            tokio::task::yield_now().await;
        }
    };
    tokio::join!(t1, t2);
}
```

В async-контексте только `tokio::sleep`. Всё остальное — блокировка для сотен задач.


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

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


---

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

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


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


### 💻 yield_now — уступаем место


```rust
use tokio::task;

async fn looper(name: &str) {
    for i in 0..3 {
        println!("{name}-{i}");
        task::yield_now().await;
    }
}

#[tokio::main]
async fn main() {
    tokio::join!(looper("A"), looper("B"));
}
```

Кооперативность: yield_now явно отдаёт квант выполнения другим задачам.


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

---

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

Перед прыжком нужно было:  
- проверить реактор,  
- перезапустить связь,  
- откалибровать навигацию.

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


### 💻 Несколько задач: join!


```rust
use tokio::time::{sleep, Duration};

async fn job(id: u32) -> u32 {
    sleep(Duration::from_millis(60 * id as u64)).await;
    println!("Задача {id} завершена");
    id * 10
}

#[tokio::main]
async fn main() {
    let (a, b) = tokio::join!(job(1), job(2));
    println!("Результаты: {a}, {b}");
}
```

Для фиксированного набора задач используйте tokio::join!



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


### 💻 Коллекция задач: join_all


```rust
use tokio::time::{sleep, Duration};
use futures::future::join_all;
async fn work(i: u32) -> u32 {
    sleep(Duration::from_millis(50 * i as u64)).await;
    i * 2
}

#[tokio::main]
async fn main() {
    let futs = vec![work(1), work(2), work(3)];
    let results = join_all(futs).await;
    println!("Результаты: {:?}", results); // [2,4,6]
}
```

Используем join_all из пакета futures
- Он ждёт коллекцию Future и возвращает Vec результатов.



{{< alert "comment" >}}
- `tokio::join!` — фиксированный набор задач → кортеж.  
- `futures::future::join_all` — коллекция задач → `Vec` результатов.  
{{< /alert >}}

---

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

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

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

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

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

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


### 💻 JoinSet: задачи по мере завершения


```rust
use tokio::task::JoinSet;
use tokio::time::{sleep, Duration};

async fn unit(i: u32) -> u32 {
    sleep(Duration::from_millis(30 * i as u64)).await;
    i * 100
}

#[tokio::main]
async fn main() {
    let mut set = JoinSet::new();
    for i in 1..=5 {
        set.spawn(unit(i));
    }

    while let Some(res) = set.join_next().await {
        match res {
            Ok(v) => println!("Готов блок: {v}"),
            Err(e) => eprintln!("Ошибка задачи: {e}"),
        }
    }
    println!("JoinSet завершил все задачи");
}
```

JoinSet позволяет получать результаты по мере готовности (в любом порядке).


---

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

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

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

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

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

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

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


{{< typeit speed=50 >}}  
Путь в многопоточность только начинается…  
{{< /typeit >}}  

---


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

{{< alert "comment" >}}  
### Шпаргалка капитана по асинхронности:

Чтобы уверенно пройти сквозь врата 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`.
{{< /alert >}}

---

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


🛰️


### 📝 Rust Async & Tokio


**1. Что выведет следующий код?**

- [ ] Привет, экипаж
- [ ] Начало потом Привет, экипаж
- [ ] Привет, экипаж потом Начало
- [ ] Начало только

**2. Что делает tokio::task::spawn?**

- [ ] Всегда создаёт новый поток ОС
- [ ] Запускает async-задачу под Tokio, возвращает JoinHandle
- [ ] Останавливает текущий поток до завершения
- [ ] Блокирует рантайм

**3. Какой результат/порядок завершения вероятен?**

- [ ] Задача 1, потом 2, потом 3
- [ ] Порядок зависит от задержек; job(1) вероятно завершится первой
- [ ] Все три одновременно
- [ ] Ошибка компиляции

**4. Чем отличается futures::future::join_all от tokio::join! ?**

- [ ] join_all работает только с двумя задачами
- [ ] join_all принимает коллекцию Future и ждёт их все
- [ ] join! всегда быстрее
- [ ] Разницы никакой

**5. Что напечатает код со spawn и ожиданием всех JoinHandle?**

- [ ] Готово 1, Готово 2, Готово 3, затем Все задачи завершены (порядок готово не гарантирован)
- [ ] Все задачи завершены напечатается раньше
- [ ] Только Готово 3
- [ ] Ошибка

**6. Что такое JoinSet в Tokio?**

- [ ] Массив синхронных функций
- [ ] Коллекция async-задач со spawn и извлечением результатов через join_next/join_all
- [ ] Функция блокирующего сна
- [ ] Особый тип потока ОС

**7. Почему thread::sleep опасен внутри async?**

- [ ] Ничем не опасен
- [ ] Блокирует воркер рантайма; другие задачи не выполняются в этом потоке
- [ ] Даёт небольшую задержку, но без влияния
- [ ] Вызывает ошибку компиляции

**8. Что выведет код со spawn двух печатателей и join! по JoinHandle?**

- [ ] A, B (в произвольном порядке), затем Завершили
- [ ] Завершили до A и B
- [ ] Только A
- [ ] Ошибка

**9. Что означает требование 'static для spawn?**

- [ ] Задача живёт вечно
- [ ] Future не должен ссылаться на не-статические данные: используйте move или владение
- [ ] Задача запускается только один раз
- [ ] Код становится синхронным

**10. Что выведет код с futures::join_all?**

- [ ] [2,4,6]
- [ ] [6,4,2]
- [ ] [1,2,3]
- [ ] Ошибка

**11. Что напечатает этот код без await на JoinHandle?**

- [ ] Фоновая задача → через секунду; потом Сразу после spawn
- [ ] Сразу после spawn напечатается сразу, фоновая задача — позже или может не успеть
- [ ] Только Фоновая задача
- [ ] Ошибка

**12. Какой порядок вывода вероятен при worker_threads=1?**

- [ ] Fast затем Slow
- [ ] Slow затем Fast
- [ ] Одновременно
- [ ] Нет гарантии

**13. Что делает join_all (futures)?**

- [ ] Ждёт завершения коллекции Future и возвращает Vec результатов
- [ ] Запускает задачи по очереди
- [ ] Делает код синхронным
- [ ] Работает только с async-std

**14. Верное объяснение async vs parallel?**

- [ ] async — это потоки ОС; parallel — ожидание
- [ ] async — кооперативная многозадачность; parallel — одновременное выполнение в нескольких потоках
- [ ] async всегда быстрее parallel
- [ ] Разницы нет

**15. Что напечатает цикл с yield_now?**

- [ ] A-0, A-1, A-2, затем B-0, B-1, B-2
- [ ] Чередование A-0, B-0, A-1, B-1, ...
- [ ] Только A
- [ ] Только B

**16. Что значит кооперативная многозадачность в Rust async?**

- [ ] Планировщик вытесняет задачу в любой момент
- [ ] Задача уступает в .await или yield_now().await
- [ ] Всегда один поток
- [ ] Только для I/O

**17. Как ведёт себя JoinSet::join_next()?**

- [ ] Возвращает в порядке launch
- [ ] Возвращает по мере завершения задач (любой порядок)
- [ ] Ждёт только первую задачу
- [ ] Работает только с spawn_blocking

**18. Какое ключевое слово делает функцию асинхронной?**

- [ ] await
- [ ] async
- [ ] spawn
- [ ] yield

    
