Tokio: runtime assíncrono
1. Introdução ao Tokio
Tokio é um runtime assíncrono orientado a eventos para Rust, projetado para construir aplicações de rede rápidas e confiáveis. Ele fornece as primitivas necessárias para escrever código concorrente eficiente utilizando async/await, com suporte a I/O não bloqueante, temporizadores e sincronização entre tasks.
A principal razão para usar Tokio é sua capacidade de lidar com milhares de conexões simultâneas usando poucas threads do sistema operacional. Diferente de modelos baseados em threads por conexão, Tokio utiliza um scheduler work-stealing que distribui tasks de forma eficiente entre as threads de trabalho.
Comparado a outros runtimes como async-std e smol, Tokio se destaca por seu ecossistema maduro, documentação extensa e integração com frameworks populares como Hyper (HTTP), Axum (web) e Tonic (gRPC). Enquanto async-std busca replicar a API padrão do Rust de forma assíncrona e smol foca em minimalismo, Tokio oferece um conjunto completo de ferramentas para produção.
2. Arquitetura do Runtime
O coração do Tokio é seu scheduler work-stealing. Quando você spawna uma task, ela é colocada em uma fila local de uma thread de trabalho. Se essa thread fica ociosa, ela "rouba" tasks de outras threads, garantindo balanceamento de carga eficiente.
Internamente, o runtime é composto por:
- Reactor: responsável por notificar quando operações de I/O estão prontas (baseado em epoll no Linux, kqueue no macOS e IOCP no Windows)
- Driver de I/O: gerencia registros de interesse em descritores de arquivo
- Timer: implementa uma roda de tempo (hierarchical timer wheel) para gerenciar temporizadores de forma eficiente
Tokio oferece dois modelos de thread:
- Multi-threaded (padrão): utiliza tokio::runtime::Runtime com múltiplas threads de trabalho
- Single-threaded: utiliza tokio::runtime::current_thread::Runtime, ideal para sistemas embarcados ou quando você precisa de controle fino sobre o threading
3. Configuração e Inicialização do Runtime
A forma mais comum de inicializar o Tokio é usando a macro #[tokio::main]:
#[tokio::main]
async fn main() {
println!("Runtime Tokio iniciado!");
}
Por baixo dos panos, essa macro expande para algo como:
fn main() {
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
println!("Runtime Tokio iniciado!");
});
}
Para configurações mais avançadas, use o Builder:
use tokio::runtime::Builder;
fn main() {
let runtime = Builder::new_multi_thread()
.worker_threads(4)
.thread_name("meu-runtime")
.enable_io()
.enable_time()
.build()
.unwrap();
runtime.block_on(async {
// seu código assíncrono aqui
});
}
Opções de configuração incluem:
- worker_threads: número de threads de trabalho
- thread_stack_size: tamanho da pilha por thread
- enable_io / enable_time: habilitar drivers específicos (útil para reduzir consumo em sistemas simples)
4. Tasks e Execução
Spawnar uma task é simples com tokio::spawn:
use tokio::task;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
// trabalho assíncrono
42
});
let resultado = handle.await.unwrap();
println!("Resultado: {}", resultado);
}
JoinHandle permite aguardar o resultado da task e capturar erros. Importante: tokio::spawn só funciona dentro de um contexto de runtime. Para spawnar de fora, use Handle::current():
use tokio::runtime::Handle;
fn funcao_sincrona() {
let handle = Handle::current();
handle.spawn(async {
println!("Task spawnada de contexto síncrono");
});
}
Para operações bloqueantes (como I/O de disco pesado), use spawn_blocking:
use tokio::task;
async fn processar_arquivo() {
let dados = task::spawn_blocking(|| {
std::fs::read("grande_arquivo.dat").unwrap()
}).await.unwrap();
// processar dados assincronamente
}
5. I/O Assíncrono com Tokio
Tokio fornece versões assíncronas dos traits Read e Write padrão: AsyncRead e AsyncWrite. Exemplo com TcpStream:
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?;
loop {
let (mut socket, _) = listener.accept().await?;
tokio::spawn(async move {
let mut buf = [0; 1024];
let n = socket.read(&mut buf).await.unwrap();
socket.write_all(&buf[..n]).await.unwrap();
});
}
}
Para buffering eficiente, use BufReader e BufWriter:
use tokio::io::{BufReader, AsyncBufReadExt};
use tokio::fs::File;
async fn ler_linhas() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("dados.txt").await?;
let reader = BufReader::new(file);
let mut linhas = reader.lines();
while let Some(linha) = linhas.next_line().await? {
println!("{}", linha);
}
Ok(())
}
6. Temporizadores e Sincronização
Temporizadores são essenciais para operações com timeout:
use tokio::time::{sleep, timeout, Duration};
async fn operacao_com_timeout() {
let resultado = timeout(Duration::from_secs(5), async {
// operação que pode demorar
sleep(Duration::from_secs(10)).await;
"pronto"
}).await;
match resultado {
Ok(valor) => println!("Sucesso: {}", valor),
Err(_) => println!("Timeout expirou!"),
}
}
Primitivas de sincronização assíncronas evitam bloquear threads:
use tokio::sync::{Mutex, Semaphore};
use std::sync::Arc;
async fn exemplo_semaforo() {
let semaphore = Arc::new(Semaphore::new(3));
let mut handles = vec![];
for i in 0..10 {
let permit = semaphore.clone().acquire_owned().await.unwrap();
handles.push(tokio::spawn(async move {
println!("Task {} executando", i);
drop(permit); // libera o semáforo
}));
}
}
Notify é útil para coordenação one-shot entre tasks:
use tokio::sync::Notify;
let notify = Arc::new(Notify::new());
let notify2 = notify.clone();
tokio::spawn(async move {
notify2.notified().await;
println!("Recebeu notificação");
});
notify.notify_one();
7. Boas Práticas e Padrões Comuns
Para estado compartilhado, prefira Arc<Mutex<T>> apenas quando necessário. Canais (mpsc, broadcast, oneshot) são geralmente mais eficientes:
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("mensagem").await.unwrap();
});
if let Some(msg) = rx.recv().await {
println!("Recebido: {}", msg);
}
}
Cancelamento seguro de tasks com CancellationToken:
use tokio_util::sync::CancellationToken;
let token = CancellationToken::new();
let token_clone = token.clone();
tokio::spawn(async move {
tokio::select! {
_ = token_clone.cancelled() => println!("Cancelado!"),
_ = alguma_operacao() => println!("Completou"),
}
});
token.cancel(); // cancela a task
Para backpressure em sistemas de I/O, use limites de concorrência com Semaphore e buffers limitados em canais.
8. Considerações Finais e Próximos Passos
Para monitoramento, use tokio-console — uma ferramenta de linha de comando que inspeciona tasks, recursos e operações do runtime em tempo real:
cargo install tokio-console
Integração com o ecossistema:
- Hyper: servidor/cliente HTTP de baixo nível
- Axum: framework web ergonômico construído sobre Tokio
- Tonic: framework gRPC com suporte a streaming
Limitações do Tokio:
- Tasks bloqueantes (CPU-bound) podem degradar o desempenho se não forem movidas para spawn_blocking
- Overhead de alocação para cada task (embora pequeno)
- Para sistemas muito simples ou embarcados, smol pode ser mais adequado
Tokio não é a melhor escolha quando:
- Você precisa de concorrência baseada em threads tradicional
- Seu workload é puramente CPU-bound sem I/O
- Você está em um ambiente sem suporte a alocação dinâmica
Com sua maturidade e ecossistema rico, Tokio é a escolha padrão para aplicações assíncronas em Rust, desde servidores web até ferramentas de linha de comando de alto desempenho.
Referências
- Documentação oficial do Tokio — Referência completa da API Tokio com exemplos e guias
- Tokio Tutorial — Tutorial oficial passo a passo do runtime assíncrono
- The Async Book (Rust) — Livro oficial sobre programação assíncrona em Rust com foco em Tokio
- Tokio Console - GitHub — Ferramenta de diagnóstico e monitoramento para Tokio
- Axum Framework — Framework web construído sobre Tokio, Hyper e Tower
- Tonic gRPC — Framework gRPC assíncrono baseado em Tokio e HTTP/2
- Tokio no Crates.io — Página oficial do crate Tokio no registro de pacotes Rust