Среда выполнения и связанные с ней вопросы

Запуск асинхронного кода

  • Явный запуск 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() {
    // Два рабочих потока
}

Альтернативные среды выполнения

Почему вы можете захотеть использовать другую среду выполнения

  1. Специфичные требования к производительности
  2. Специализированные use-cases (встраиваемые системы, WASM)
  3. Экспериментальные функции
  4. Упрощение зависимостей

Какие вариации существуют на высоком уровне проектирования

Типы планировщиков:

  • 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 среда выполнения

Ссылка вперед на продвинутые главы

Более подробное обсуждение реализации сред выполнения, создания собственных планировщиков и продвинутых паттернов будет в продвинутых главах этого руководства.