Tokio для Rust: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Tokio и зачем он нужен?

Tokio — это асинхронный рантайм для языка программирования Rust. Он предоставляет инфраструктуру для написания быстрых, надёжных и масштабируемых сетевых приложений. Если вы пишете на Rust что-то, что работает с сетью, файлами или требует параллельной обработки запросов без блокировки потоков — вам нужен Tokio.

В отличие от традиционного многопоточного подхода, где каждый запрос обрабатывается в отдельном потоке ОС (что дорого и ограничено), Tokio использует модель асинхронного ввода-вывода на основе событий. Один поток может обрабатывать тысячи одновременно открытых соединений, переключаясь между задачами только тогда, когда текущая задача ожидает (например, чтения из сокета).

Tokio — это не просто библиотека, это экосистема. В её состав входят:

  • Рантайм — планировщик задач и драйверы событий (epoll/kqueue/IOCP)
  • Стандартные сетевые протоколы — TCP, UDP, Unix sockets
  • Таймеры — sleep, interval, deadline
  • Синхронизация — мьютексы, семафоры, барьеры для асинхронного кода
  • Файловый ввод-вывод — асинхронное чтение/запись файлов
  • Утилиты — каналы (mpsc, oneshot, broadcast), буферы, кодек

Tokio лежит в основе таких известных проектов, как веб-фреймворк Axum, HTTP-клиент Reqwest, веб-сокеты Tungstenite, базы данных SQLx и многих других.

Установка

В отличие от Python (pip) или Node.js (npm), Rust использует систему пакетов Cargo. Для установки Tokio добавьте зависимость в файл Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }

Флаг features = ["full"] включает все возможности Tokio. Для production-проектов рекомендуется выбирать только нужные фичи, чтобы уменьшить время компиляции и размер бинарника:

[dependencies]
tokio = { version = "1", features = ["rt", "macros", "net", "io-util", "time", "sync"] }

Основные фичи:

  • rt — многопоточный рантайм
  • macros — макросы #[tokio::main] и tokio::select!
  • net — TCP, UDP, Unix sockets
  • io-util — асинхронные Reader/Writer
  • time — таймеры и sleep
  • sync — примитивы синхронизации
  • fs — асинхронная работа с файловой системой
  • process — асинхронный запуск команд
  • signal — обработка сигналов ОС

Быстрый старт

Создадим простой HTTP-сервер, который отвечает "Hello, World!" на любой запрос. Для этого нам понадобится также крейт hyper (HTTP-библиотека поверх Tokio):

// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
// hyper = { version = "1", features = ["server", "http1"] }

use hyper::service::service_fn;
use hyper::{Request, Response, Server};
use hyper::body::Incoming;
use std::convert::Infallible;

async fn handle_request(_req: Request<Incoming>) -> Result<Response<String>, Infallible> {
    Ok(Response::new("Hello, World!".to_string()))
}

#[tokio::main]
async fn main() {
    let addr = ([127, 0, 0, 1], 3000).into();
    let server = Server::bind(&addr).serve(service_fn(handle_request));
    println!("Server running on http://{}", addr);
    if let Err(e) = server.await {
        eprintln!("Server error: {}", e);
    }
}

Запустите cargo run и откройте http://127.0.0.1:3000 в браузере. Вы увидите "Hello, World!".

Основные методы и функции

1. tokio::spawn

Что делает: Запускает новую асинхронную задачу в фоновом режиме. Задача будет выполняться параллельно с другими задачами в рамках одного рантайма.

Параметры:

  • future — асинхронная функция или блок кода, реализующий трейт Future

Возвращает: JoinHandle<T> — дескриптор, который можно ожидать (.await) для получения результата.

use tokio::task;

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // Тяжелая работа в фоне
        let result = 42 + 10;
        println!("Задача выполнена, результат: {}", result);
        result
    });

    // Можно делать что-то ещё, пока задача выполняется
    println!("Главная задача продолжается...");

    // Ожидаем завершения фоновой задачи
    let value = handle.await.unwrap();
    println!("Получен результат из задачи: {}", value);
}

2. tokio::time::sleep

Что делает: Приостанавливает выполнение текущей асинхронной задачи на указанный промежуток времени. В отличие от std::thread::sleep, не блокирует поток — другие задачи могут выполняться во время ожидания.

Параметры:

  • duration — std::time::Duration или core::time::Duration

Возвращает: Sleep — будущее, которое завершается по истечении времени.

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    println!("Начало ожидания...");
    sleep(Duration::from_secs(2)).await;
    println!("Прошло 2 секунды!");

    // Можно использовать Duration::from_millis
    sleep(Duration::from_millis(500)).await;
    println!("Ещё 500 мс прошло");
}

3. tokio::net::TcpListener

Что делает: Создаёт TCP-сервер, который слушает входящие соединения на указанном адресе. Асинхронно принимает новые подключения.

Параметры конструктора:

  • addr — строка или SocketAddr (например, "127.0.0.1:8080")

Методы:

  • accept() — принимает новое соединение, возвращает (TcpStream, SocketAddr)
  • bind(addr) — статический метод для создания слушателя
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;
    println!("Сервер запущен на порту 8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        println!("Новое подключение от: {}", addr);

        tokio::spawn(async move {
            let mut buf = [0; 1024];
            // Читаем данные от клиента
            let n = socket.read(&mut buf).await.unwrap();
            // Отправляем обратно (эхо-сервер)
            socket.write_all(&buf[..n]).await.unwrap();
        });
    }
}

4. tokio::io::AsyncReadExt::read_to_string

Что делает: Асинхронно читает все данные из источника (файл, сокет, поток) до конца и помещает их в строку. Это расширение трейта AsyncReadExt.

Параметры:

  • buf — изменяемая ссылка на String, куда будут записаны данные

Возвращает: io::Result<usize> — количество прочитанных байт.

use tokio::fs::File;
use tokio::io::AsyncReadExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = File::open("example.txt").await?;
    let mut contents = String::new();
    let bytes_read = file.read_to_string(&mut contents).await?;
    println!("Прочитано {} байт:", bytes_read);
    println!("{}", contents);
    Ok(())
}

5. tokio::sync::Mutex

Что делает: Асинхронный мьютекс для защиты общих данных между несколькими задачами. В отличие от стандартного std::sync::Mutex, его можно использовать внутри .await.

Параметры:

  • value — начальное значение, которое будет защищено мьютексом

Методы:

  • lock() — захватывает блокировку, возвращает MutexGuard<T>
  • try_lock() — пытается захватить блокировку без ожидания
use tokio::sync::Mutex;
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for i in 0..10 {
        let counter = Arc::clone(&counter);
        handles.push(tokio::spawn(async move {
            let mut num = counter.lock().await;
            *num += 1;
            println!("Задача {} увеличила счётчик до {}", i, *num);
        }));
    }

    for handle in handles {
        handle.await.unwrap();
    }

    let final_count = counter.lock().await;
    println!("Итоговое значение: {}", *final_count);
}

6. tokio::sync::mpsc::channel

Что делает: Создаёт многопроизводительный, однопотребительский канал (Multi-Producer, Single-Consumer) для передачи данных между задачами.

Параметры:

  • buffer — размер буфера канала (количество сообщений, которые могут быть в очереди)

Возвращает: Кортеж (Sender<T>, Receiver<T>).

use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel::<String>(32);

    // Производитель (можно клонировать tx для нескольких отправителей)
    let tx1 = tx.clone();
    tokio::spawn(async move {
        tx1.send("Привет из задачи 1".to_string()).await.unwrap();
    });

    tokio::spawn(async move {
        tx.send("Привет из задачи 2".to_string()).await.unwrap();
    });

    // Потребитель
    while let Some(msg) = rx.recv().await {
        println!("Получено: {}", msg);
    }
}

7. tokio::select!

Что делает: Макрос, который ожидает завершения нескольких асинхронных операций одновременно и выполняет ветку, соответствующую первой завершившейся операции.

Параметры:

  • Одна или несколько веток

Рекомендаций