Что такое Tokio и какую проблему он решает в Rust?
Tokio — async runtime для Rust: планировщик задач (task scheduler), event loop на основе epoll/kqueue, пул потоков и богатая экосистема (TCP, таймеры, каналы). Решает проблему масштабируемого I/O без блокировки OS-потоков.
Что такое Tokio
Tokio — это асинхронный runtime для Rust, реализующий цикл событий (event loop) поверх epoll (Linux), kqueue (macOS) и IOCP (Windows). Он состоит из трёх слоёв:
- Reactor — слушает события ОС (сокеты готовы к чтению/записи) и пробуждает нужные
Future. - Executor (scheduler) — работает на пуле OS-потоков (
work-stealing) и раскручиваетFutureдо следующей точки ожидания. - Экосистема —
tokio::net,tokio::time,tokio::sync,tokio::fs: готовые async-примитивы поверх runtime.
Какую проблему решает
Rust без runtime поддерживает синтаксис async/await, но компилятор генерирует лишь конечный автомат (Future) — выполнять его некому. Tokio предоставляет этого «исполнителя». Альтернатива без async — один поток на соединение (модель Apache), что при 10 000 одновременных клиентов означает 10 000 OS-потоков и сотни МБ стека. Tokio мультиплексирует тысячи задач на небольшом пуле потоков (по умолчанию равном числу CPU), экономя память и избегая context switch OS.
Практический пример
HTTP-сервер, принимающий соединения и отвечающий за 1 мс:
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main] // разворачивается в Runtime::new().block_on(...)
async fn main() -> anyhow::Result<()> {
let listener = TcpListener::bind("0.0.0.0:8080").await?;
println!("Listening on :8080");
loop {
let (mut socket, addr) = listener.accept().await?;
// tokio::spawn порождает лёгкую задачу (green thread),
// НЕ OS-поток — стек ~2 KB вместо ~8 MB
tokio::spawn(async move {
let mut buf = vec![0u8; 1024];
match socket.read(&mut buf).await {
Ok(0) => return, // соединение закрыто
Ok(n) => {
println!("Got {} bytes from {}", n, addr);
let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
let _ = socket.write_all(response).await;
}
Err(e) => eprintln!("read error: {}", e),
}
});
}
}
Ключевые моменты: #[tokio::main] запускает многопоточный runtime; tokio::spawn — неблокирующий запуск задачи; .await — точка, где executor передаёт управление другим задачам.
Многопоточный vs однопоточный runtime
Tokio предлагает два режима:
#[tokio::main]—multi_threadпо умолчанию, N рабочих потоков (N = CPU).#[tokio::main(flavor = "current_thread")]— один поток, полезен для тестов и встраиваемых систем.
// Явная конфигурация runtime
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()?;
rt.block_on(async { /* ... */ });
Подводные камни
- Блокирующий код в async-контексте. Вызов
std::thread::sleepили синхронного I/O внутриasyncблокирует весь рабочий поток executor, заморозив все задачи на нём. Решение:tokio::time::sleepдля таймеров,tokio::task::spawn_blockingдля CPU-heavy и синхронного кода. - Паника в spawned task молча проглатывается.
tokio::spawnвозвращаетJoinHandle; если его не.await-ить, паника потеряется. Всегда обрабатывайтеJoinHandle::awaitили логируйте ошибки. - Отсутствие
Sendу Future. Multi-thread runtime требует, чтобы задачи реализовывалиSend. ХранениеRc,RefCellили сырых указателей через точку.awaitвызовет ошибку компилятора. - Неправильный выбор канала.
tokio::sync::Mutexнельзя удерживать через.awaitесли используется std Mutex (deadlock). Нужен именноtokio::sync::Mutexдля async контекстов. - Пропущенный
enable_all(). При ручной сборке runtime без.enable_all()или.enable_io().enable_time()— TCP-сокеты и таймеры не работают (panic в runtime). - Task cancellation и утечки ресурсов. Когда
JoinHandleдропается, задача отменяется при следующем.await. Деструкторы вызываются, но если ресурс освобождается после последней точки.await, cleanup не выполнится. Используйтеtokio::select!с явными ветками отмены. - Вложенные runtime. Вызов
Runtime::block_onвнутри уже работающего Tokio runtime вызывает panic. При интеграции синхронного и async кода используйтеHandle::current().block_on()илиspawn_blocking. - Версионная несовместимость. Tokio 0.x и Tokio 1.x несовместимы на уровне типов. Если зависимость тянет
tokio 0.2, а ваш код используетtokio 1.x, компилятор выдаст ошибки несовпадения типов дляFuture.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.