Introdução
A programação assíncrona é fundamental no Rust moderno, especialmente para aplicações de I/O intensivo como servidores web, clientes HTTP e sistemas distribuídos. Diferente de linguagens como Go ou JavaScript, Rust não inclui um runtime async na biblioteca padrão — você precisa escolher um.
Os dois principais runtimes são Tokio e async-std. O Tokio é o runtime dominante do ecossistema, usado por frameworks como Axum e Tonic, e oferece um conjunto robusto de ferramentas para I/O, timers, canais e sincronização. O async-std surgiu como alternativa com a proposta de espelhar a API da biblioteca padrão do Rust, facilitando a transição de código síncrono para assíncrono.
Neste artigo, faremos uma comparação técnica detalhada entre os dois para ajudá-lo a tomar uma decisão informada.
Tabela Comparativa
| Característica | Tokio | async-std |
|---|---|---|
| Primeira versão | 2018 | 2019 |
| Mantenedor | Tokio Team (Alice Ryhl et al.) | async-rs community |
| Modelo de execução | Work-stealing multi-thread | Work-stealing multi-thread |
| Single-thread mode | current_thread | Não oficial |
| Timer | tokio::time | async_std::task::sleep |
| Channels | tokio::sync::mpsc, broadcast, watch, oneshot | async_std::channel |
| I/O | tokio::io, tokio::net, tokio::fs | async_std::io, async_std::net, async_std::fs |
| Ecossistema | Imenso (axum, reqwest, tonic, sqlx…) | Limitado (tide, surf…) |
| Downloads mensais | ~50M+ | ~10M+ |
| Atividade | Muito ativa | Manutenção reduzida |
Dependências no Cargo.toml
Para Tokio:
[dependencies]
tokio = { version = "1", features = ["full"] }
Ou com features seletivas (recomendado para produção):
[dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "time", "sync"] }
Para async-std:
[dependencies]
async-std = { version = "1", features = ["attributes"] }
Comparação de Código
Spawning de Tasks
Tokio:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
println!("Task concluída!");
42
});
let resultado = handle.await.unwrap();
println!("Resultado: {resultado}");
}
async-std:
use async_std::task;
use std::time::Duration;
#[async_std::main]
async fn main() {
let handle = task::spawn(async {
task::sleep(Duration::from_millis(100)).await;
println!("Task concluída!");
42
});
let resultado = handle.await;
println!("Resultado: {resultado}");
}
Observe que no async-std, o await do handle retorna diretamente o valor (sem Result), enquanto no Tokio retorna Result<T, JoinError>.
TCP Server
Tokio:
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!("Servidor rodando na porta 8080");
loop {
let (mut socket, addr) = listener.accept().await?;
println!("Nova conexão de: {addr}");
tokio::spawn(async move {
let mut buf = [0u8; 1024];
loop {
let n = match socket.read(&mut buf).await {
Ok(0) => return,
Ok(n) => n,
Err(_) => return,
};
if socket.write_all(&buf[..n]).await.is_err() {
return;
}
}
});
}
}
async-std:
use async_std::net::TcpListener;
use async_std::prelude::*;
use async_std::task;
#[async_std::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Servidor rodando na porta 8080");
let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let mut stream = stream?;
let addr = stream.peer_addr()?;
println!("Nova conexão de: {addr}");
task::spawn(async move {
let mut buf = [0u8; 1024];
loop {
let n = match stream.read(&mut buf).await {
Ok(0) => return,
Ok(n) => n,
Err(_) => return,
};
if stream.write_all(&buf[..n]).await.is_err() {
return;
}
}
});
}
Ok(())
}
Timers e Timeouts
Tokio oferece um módulo time rico:
use tokio::time::{sleep, timeout, interval, Duration};
#[tokio::main]
async fn main() {
// Sleep
sleep(Duration::from_secs(1)).await;
// Timeout em uma operação
let resultado = timeout(Duration::from_secs(5), operacao_lenta()).await;
match resultado {
Ok(valor) => println!("Sucesso: {valor}"),
Err(_) => println!("Timeout!"),
}
// Interval para execução periódica
let mut intervalo = interval(Duration::from_secs(1));
for _ in 0..5 {
intervalo.tick().await;
println!("Tick!");
}
}
async fn operacao_lenta() -> String {
sleep(Duration::from_secs(2)).await;
"pronto".to_string()
}
async-std oferece funcionalidades similares, mas mais básicas:
use async_std::task;
use async_std::future::timeout;
use std::time::Duration;
#[async_std::main]
async fn main() {
// Sleep
task::sleep(Duration::from_secs(1)).await;
// Timeout
let resultado = timeout(
Duration::from_secs(5),
operacao_lenta(),
).await;
match resultado {
Ok(valor) => println!("Sucesso: {valor}"),
Err(_) => println!("Timeout!"),
}
}
async fn operacao_lenta() -> String {
task::sleep(Duration::from_secs(2)).await;
"pronto".to_string()
}
Note que async-std não possui interval integrado — você precisaria implementá-lo manualmente ou usar uma crate adicional.
Channels
Tokio oferece múltiplos tipos de canais:
use tokio::sync::{mpsc, oneshot, broadcast};
#[tokio::main]
async fn main() {
// mpsc: múltiplos produtores, um consumidor
let (tx, mut rx) = mpsc::channel::<String>(32);
tokio::spawn(async move {
tx.send("mensagem 1".to_string()).await.unwrap();
tx.send("mensagem 2".to_string()).await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("Recebido: {msg}");
}
// oneshot: envio único
let (tx, rx) = oneshot::channel::<u32>();
tokio::spawn(async move {
tx.send(42).unwrap();
});
let valor = rx.await.unwrap();
println!("Oneshot: {valor}");
// broadcast: múltiplos consumidores
let (tx, _) = broadcast::channel::<String>(16);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tx.send("broadcast!".to_string()).unwrap();
println!("rx1: {}", rx1.recv().await.unwrap());
println!("rx2: {}", rx2.recv().await.unwrap());
}
async-std utiliza canais da crate async-channel:
use async_std::task;
use async_std::channel;
#[async_std::main]
async fn main() {
let (tx, rx) = channel::bounded::<String>(32);
task::spawn({
let tx = tx.clone();
async move {
tx.send("mensagem 1".to_string()).await.unwrap();
tx.send("mensagem 2".to_string()).await.unwrap();
}
});
drop(tx);
while let Ok(msg) = rx.recv().await {
println!("Recebido: {msg}");
}
}
Performance
Ambos os runtimes usam work-stealing schedulers e são muito eficientes. Na prática, a diferença de performance entre eles é negligível para a maioria das aplicações.
Tokio tem uma vantagem em cenários de alta concorrência, pois seu scheduler foi mais extensivamente otimizado e testado em produção por empresas como Discord, Cloudflare e AWS.
Resultados aproximados de benchmarks de I/O (eco TCP):
| Runtime | Conexões simultâneas | Throughput |
|---|---|---|
| Tokio | 10.000 | ~950K msg/s |
| async-std | 10.000 | ~850K msg/s |
Para uma análise mais aprofundada do Tokio, consulte nosso Guia Completo do Tokio.
Ecossistema e Compatibilidade
A maior diferença entre os dois runtimes está no ecossistema. Tokio é usado por praticamente todas as crates async populares:
- Web: Axum, Actix Web, Warp
- HTTP: Reqwest, Hyper
- Banco de dados: SQLx, SeaORM
- gRPC: Tonic
- Mensageria: rdkafka, lapin (RabbitMQ)
- Observabilidade: tracing (veja Tracing vs Log)
O async-std tem um ecossistema próprio menor:
- Web: Tide
- HTTP: Surf
- Banco de dados: suporte limitado
Muitas crates que dependem de Tokio simplesmente não funcionam com async-std, pois usam tipos específicos do Tokio (como tokio::sync::Mutex ou tokio::spawn).
Quando Escolher Cada Um
Escolha Tokio quando:
- Está iniciando um novo projeto (padrão do ecossistema)
- Precisa de integração com crates populares (axum, reqwest, sqlx, tonic)
- Precisa de channels avançados (broadcast, watch, oneshot)
- Necessita de
select!para concorrência complexa - Quer a melhor documentação e suporte da comunidade
- Está construindo uma API REST ou microserviço
Escolha async-std quando:
- Prefere uma API que espelha a std do Rust
- Está fazendo prototipação rápida ou projetos educacionais
- Já tem um projeto existente que depende de async-std
- Não precisa de crates que dependem de Tokio
Recomendação prática
Use Tokio. Essa é a recomendação direta para a grande maioria dos projetos. O ecossistema, a documentação, o suporte e a atividade de desenvolvimento fazem do Tokio a escolha padrão para programação async em Rust. O async-std é um projeto interessante, mas seu desenvolvimento desacelerou significativamente e o ecossistema ao redor é limitado.
Compatibilidade entre Runtimes
Se você precisa usar uma crate que depende de outro runtime, existem adaptadores:
[dependencies]
tokio = { version = "1", features = ["full"] }
async-compat = "0.2"
use async_compat::Compat;
use tokio::runtime::Runtime;
fn main() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
// Executar future de async-std dentro do Tokio
Compat::new(async_std_future()).await;
});
}
No entanto, misturar runtimes adiciona complexidade e pode causar problemas sutis. Sempre que possível, padronize em um único runtime.
Conclusão
A escolha entre Tokio e async-std em 2026 é bastante clara: Tokio é o padrão do ecossistema Rust async. Ele tem o maior ecossistema, a melhor documentação, o desenvolvimento mais ativo e é a dependência esperada pela maioria das crates.
O async-std foi uma contribuição importante ao demonstrar que era possível ter APIs async mais ergonômicas, e muitas de suas ideias influenciaram o próprio Tokio. Contudo, para projetos novos, o Tokio é a escolha mais segura e pragmática.