В чём разница между однопоточным и многопоточным runtime в Tokio?
current_thread запускает всё в одном OS-потоке (можно Rc/RefCell, нет Send-ограничений); multi_thread создаёт пул worker-потоков с work-stealing для параллельных I/O и CPU-задач, но требует Send + 'static.
Два режима Tokio Runtime
Tokio предоставляет два варианта runtime: однопоточный (current_thread) и многопоточный (multi_thread). Выбор влияет на то, сколько OS-потоков используется, как задачи планируются и какие типы данных можно безопасно использовать в async-коде.
Однопоточный runtime (current_thread)
Весь async-код выполняется в одном OS-потоке. Планировщик — кооперативный: задачи уступают управление только при .await. Это означает, что данные не обязаны быть Send — можно использовать Rc<T>, RefCell<T> и другие не-Send типы прямо в futures.
use tokio::runtime::Builder;
fn main() {
let rt = Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
// Rc можно использовать, т.к. один поток
let data = std::rc::Rc::new(42);
println!("value: {}", data);
});
}
Макрос #[tokio::test] по умолчанию использует current_thread, что упрощает написание тестов без многопоточных гонок.
Многопоточный runtime (multi_thread)
По умолчанию запускает N worker-потоков (обычно равно числу логических CPU). Задачи могут мигрировать между потоками через work-stealing планировщик. Все типы, помещаемые в задачи, должны быть Send + 'static.
use tokio::runtime::Builder;
fn main() {
let rt = Builder::new_multi_thread()
.worker_threads(4) // явно задаём число потоков
.thread_name("my-worker")
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let handle = tokio::spawn(async {
// этот код может выполниться на любом из 4 потоков
heavy_computation().await
});
handle.await.unwrap();
});
}
async fn heavy_computation() -> u64 {
tokio::task::spawn_blocking(|| {
// CPU-bound работа вне async-контекста
(0..1_000_000u64).sum()
})
.await
.unwrap()
}
Атрибут #[tokio::main]
По умолчанию #[tokio::main] разворачивается в multi_thread. Для явного указания:
// Многопоточный (по умолчанию)
#[tokio::main]
async fn main() { /* ... */ }
// Однопоточный
#[tokio::main(flavor = "current_thread")]
async fn main() { /* ... */ }
// Многопоточный с явным числом потоков
#[tokio::main(worker_threads = 2)]
async fn main() { /* ... */ }
Когда что использовать
- current_thread — CLI-инструменты, тесты, embedded-системы, задачи с не-
Sendданными (например,Rc, FFI-указатели) - multi_thread — HTTP-серверы (Axum, Actix), gRPC-сервисы, любые workload с параллельными I/O-запросами или CPU-bound задачами через
spawn_blocking
Подводные камни
- Долгая синхронная работа (
std::thread::sleep, тяжёлые вычисления) вcurrent_threadблокирует весь runtime — другие задачи не получат CPU до завершения - В
multi_threadнельзя хранитьRc<T>илиCell<T>через точку.await— компилятор выдаст ошибку «does not implement Send» spawn_blockingсоздаёт отдельный пул потоков (по умолчанию до 512); при злоупотреблении можно исчерпать OS-потоки- Work-stealing в
multi_threadможет вызвать cache thrashing при очень маленьких задачах — лучше батчить мелкую работу - При создании нескольких runtime через
Builderкаждый держит свои OS-потоки — легко непреднамеренно создать избыток потоков tokio::runtime::Handle::current()паникует вне контекста runtime — нельзя вызывать из обычного синхронного кода безblock_on- Вложенный
block_on(вызовrt.block_onвнутри уже выполняющегося async-кода) вызывает панику — используйтеtokio::spawnвместо этого
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.