Среда выполнения и связанные с ней вопросы
Запуск асинхронного кода
- Явный запуск vs async main
- Концепция контекста tokio
- block_on
- Среда выполнения, отраженная в коде (Runtime, Handle)
- Завершение работы среды выполнения
Потоки и задачи
- Работа по умолчанию с кражеством задач (work stealing), многопоточность
- повторное рассмотрение ограничений Send + 'static
- yield
- spawn-local
- spawn-blocking (повторение), block-in-place
- Специфичные для tokio особенности по уступке другим потокам, локальные vs глобальные очереди и т.д.
Параметры конфигурации
- Размер пула потоков
- Однопоточность, один поток на ядро и т.д.
Альтернативные среды выполнения
- Почему вы можете захотеть использовать другую среду выполнения или реализовать свою собственную
- Какие вариации существуют на высоком уровне проектирования
- Ссылка вперед на продвинутые главы
Среда выполнения (runtime) является сердцем любой асинхронной программы на Rust. Она отвечает за планирование и выполнение асинхронных задач. В этом разделе мы подробно рассмотрим, как работают среды выполнения и с какими проблемами можно столкнуться.
Запуск асинхронного кода
Явный запуск vs async main
Существует два основных подхода к запуску асинхронного кода:
Async main (использование макроса):
#[tokio::main] async fn main() { // Асинхронный код some_async_function().await; }
Макрос #[tokio::main] разворачивается в код, который создает среду выполнения и запускает асинхронную функцию main.
Явный запуск:
fn main() { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { // Асинхронный код some_async_function().await; }); }
Явное создание среды выполнения дает больше контроля над конфигурацией.
Концепция контекста tokio
Tokio использует концепцию thread-local контекста для отслеживания текущей среды выполнения. Это позволяет функциям вроде tokio::spawn работать без явной передачи ссылки на среду выполнения.
#[tokio::main] async fn main() { // Внутри этого блока доступен контекст Tokio tokio::spawn(async { // Эта задача будет запущена в контексте текущей среды выполнения }); }
block_on
Функция block_on блокирует текущий поток до завершения фьючерса:
#![allow(unused)] fn main() { use tokio::runtime::Runtime; let rt = Runtime::new().unwrap(); let result = rt.block_on(async { some_async_function().await }); }
Важно: block_on не должна использоваться внутри асинхронного контекста, так как это может привести к взаимоблокировкам.
Среда выполнения, отраженная в коде (Runtime, Handle)
Runtime - это основная структура, представляющая среду выполнения Tokio. Она владеет рабочими потоками и ресурсами.
Handle - это легковесная ссылка на среду выполнения, которая может быть клонирована и передана между задачами:
#[tokio::main] async fn main() { let handle = tokio::runtime::Handle::current(); // Handle можно использовать для запуска задач извне async контекста std::thread::spawn(move || { handle.block_on(async { tokio::spawn(async { /* ... */ }); }); }); }
Завершение работы среды выполнения
При завершении работы среды выполнения важно правильно закрыть все ресурсы:
use tokio::runtime::Runtime; fn main() -> Result<(), Box<dyn std::error::Error>> { let rt = Runtime::new()?; // Запускаем основную логику rt.block_on(async { // Асинхронный код }); // Runtime автоматически завершает работу при выходе из области видимости // Можно также явно вызвать shutdown_timeout // rt.shutdown_timeout(Duration::from_secs(30)); Ok(()) }
Потоки и задачи
Работа по умолчанию с кражеством задач (work stealing), многопоточность
По умолчанию Tokio использует многопоточный планировщик с кражеством задач (work-stealing). Это означает:
- Каждый рабочий поток имеет свою локальную очередь задач
- Если у потока нет задач, он может "украсть" задачи из очередей других потоков
- Это обеспечивает хорошую балансировку нагрузки
Повторное рассмотрение ограничений Send + 'static
Из-за многопоточной природы среды выполнения Tokio, задачи должны быть Send + 'static:
#![allow(unused)] fn main() { // Эта задача может быть запущена в любом потоке tokio::spawn(async { // Код здесь должен быть Send }); // Эта НЕ может быть запущена через spawn let local_data = Rc::new(42); tokio::spawn(async { // ОШИБКА: Rc не является Send // println!("{}", local_data); }); }
yield
Функция yield_now позволяет задаче добровольно уступить управление планировщику:
#![allow(unused)] fn main() { async fn cooperative_task() { for i in 0..100 { if i % 10 == 0 { tokio::task::yield_now().await; } // Тяжелые вычисления } } }
spawn-local
Для задач, которые не являются Send, используйте spawn_local:
use tokio::task; #[tokio::main] async fn main() { let local_data = Rc::new(42); task::spawn_local(async move { // Это работает, так как задача выполняется в текущем потоке println!("{}", local_data); }).await.unwrap(); }
Ограничение: spawn_local требует, чтобы текущий поток был настроен для локального запуска задач.
spawn-blocking (повторение), block-in-place
spawn_blocking запускает блокирующий код в отдельном пуле потоков:
#![allow(unused)] fn main() { let result = tokio::task::spawn_blocking(|| { // Блокирующие вычисления или IO std::thread::sleep(Duration::from_secs(1)); 42 }).await?; }
block_in_place временно освобождает текущий рабочий поток для выполнения блокирующего кода:
#![allow(unused)] fn main() { tokio::task::block_in_place(|| { // Блокирующий код std::thread::sleep(Duration::from_secs(1)); }); }
Специфичные для tokio особенности
Локальные vs глобальные очереди:
- Каждый рабочий поток имеет локальную очередь LIFO
- Есть общая глобальная очередь
- Задачи из локальной очереди имеют приоритет
Уступка другим потокам: При уступке управления (yield_now), задача может быть перемещена в глобальную очередь, что дает другим потокам возможность ее выполнить.
Параметры конфигурации
Размер пула потоков
#![allow(unused)] fn main() { use tokio::runtime; let rt = runtime::Builder::new_multi_thread() .worker_threads(4) // Количество рабочих потоков .max_blocking_threads(100) // Максимум потоков для блокирующих задач .enable_time() // Включить поддержку времени .enable_io() // Включить поддержку IO .build()?; }
Однопоточность, один поток на ядро и т.д.
Однопоточная среда выполнения:
#[tokio::main(flavor = "current_thread")] async fn main() { // Все задачи выполняются в одном потоке }
Многопоточная с фиксированным количеством потоков:
#[tokio::main(flavor = "multi_thread", worker_threads = 2)] async fn main() { // Два рабочих потока }
Альтернативные среды выполнения
Почему вы можете захотеть использовать другую среду выполнения
- Специфичные требования к производительности
- Специализированные use-cases (встраиваемые системы, WASM)
- Экспериментальные функции
- Упрощение зависимостей
Какие вариации существуют на высоком уровне проектирования
Типы планировщиков:
- Work-stealing (Tokio)
- Fixed thread pool
- Single-threaded
- Actor-based
Модели ввода-вывода:
- epoll (Linux)
- kqueue (macOS, BSD)
- IOCP (Windows)
- io_uring (современный Linux)
Примеры альтернативных сред выполнения:
- async-std: Альтернатива с другим API дизайном
- smol: Минималистичная среда выполнения
- glommio: Среда выполнения с ориентацией на производительность, использующая io_uring
- bastion: Actor-based среда выполнения
Ссылка вперед на продвинутые главы
Более подробное обсуждение реализации сред выполнения, создания собственных планировщиков и продвинутых паттернов будет в продвинутых главах этого руководства.