Что такое 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 socketsio-util— асинхронные Reader/Writertime— таймеры и sleepsync— примитивы синхронизации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!
Что делает: Макрос, который ожидает завершения нескольких асинхронных операций одновременно и выполняет ветку, соответствующую первой завершившейся операции.
Параметры:
- Одна или несколько веток