TokioMiddleExperience

Какую инженерную проблему Tokio (Rust) решает в build/runtime/systems-разработке, если не сводить ответ к определению?

Tokio решает проблему масштабируемого I/O без потоков ОС: один поток work-stealing планировщика ведёт тысячи задач, устраняя накладные расходы на переключение контекста и синхронизацию mutex-ов на уровне ОС.

Инженерная проблема, которую решает Tokio

Классический подход «один поток на соединение» упирается в лимиты ОС: каждый поток стоит ~8 МБ RSS и несколько микросекунд на context switch. При 10 000 одновременных соединений это уже ~80 ГБ и постоянные переключения. Tokio устраняет эту проблему через кооперативное мультиплексирование задач поверх небольшого пула потоков.

Что конкретно происходит внутри

  • Work-stealing scheduler: пул из N потоков (по умолчанию — число CPU). Каждый поток имеет локальную деку задач. Если она пуста, поток «крадёт» задачи у соседей, минимизируя простой.
  • Async I/O через epoll/kqueue/IOCP: системные вызовы переведены в неблокирующий режим. Когда задача ожидает данные из сокета, она возвращает управление планировщику (Poll::Pending), а не блокирует поток.
  • Waker API: когда ядро сигнализирует о готовности дескриптора, runtime пробуждает конкретную задачу через её Waker и помещает её обратно в деку.
  • Task = stackless coroutine: состояние хранится в heap-аллоцированном state machine, а не в стеке потока. Это позволяет держать миллионы задач с минимальным потреблением памяти.

Пример: эхо-сервер на 100k соединений

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:8080").await?;
    loop {
        let (mut socket, addr) = listener.accept().await?;
        // каждое соединение — отдельная task, не поток
        tokio::spawn(async move {
            let mut buf = vec![0u8; 4096];
            loop {
                match socket.read(&mut buf).await {
                    Ok(0) => break,
                    Ok(n) => {
                        if socket.write_all(&buf[..n]).await.is_err() {
                            break;
                        }
                    }
                    Err(_) => break,
                }
            }
            println!("Connection closed: {addr}");
        });
    }
}

Здесь 100 000 одновременных соединений займут ~100 000 задач, но физических потоков — лишь 8 (по числу CPU). Стандартный thread-per-connection подход при тех же условиях требовал бы 800 ГБ RSS.

Почему это важно для build/systems-разработки

  • Компилятор Rust проверяет Send/Sync для задач — race conditions на этапе сборки, а не в продакшне.
  • Tokio интегрируется с tracing и tokio-console для live-диагностики задач без перезапуска.
  • Нулевая стоимость абстракций: если async-функция не используется конкурентно, компилятор может оптимизировать её до простого синхронного кода.

Подводные камни

  • Blocking код в async-контексте: вызов std::thread::sleep или синхронного I/O блокирует весь worker-поток. Используйте tokio::time::sleep и tokio::task::spawn_blocking.
  • CPU-bound задачи голодают планировщик: длинный цикл без .await не отдаёт управление. Добавляйте tokio::task::yield_now().await или переносите в spawn_blocking.
  • Нельзя смешивать рантаймы: вызов async-кода одного Tokio runtime из другого вызывает панику «cannot start a runtime from within a runtime».
  • Забытый JoinHandle: если не .await хендл и не сохранить его, задача будет работать до завершения runtime, утекая ресурсы.
  • Неправильный размер пула: #[tokio::main] по умолчанию создаёт multi-thread runtime. Для CLI-утилит без конкурентности лучше #[tokio::main(flavor = "current_thread")].
  • Panic в spawn не propagate-ится: паника внутри tokio::spawn не падает родительскую задачу, а возвращается через JoinHandle::await как Err(JoinError).
  • Starvation при несбалансированном spawn: если одна задача создаёт тысячи дочерних без контроля, очередь растёт бесконтрольно. Используйте tokio::sync::Semaphore для rate-limiting.

What hurts your answer

  • Знать термины Tokio (Rust), но не понимать связи между абстракциями
  • Объяснять поведение через отдельные примеры вместо причинной модели
  • Не связывать mental model с диагностикой ошибок

What they're listening for

  • Понимает ключевые абстракции Tokio (Rust)
  • Может предсказывать поведение системы через mental model
  • Связывает модель с debugging и production decisions

Related topics