Tokio vs Async-std: Qual Runtime Rust Usar? | Rust Brasil

Tokio vs async-std: comparação de runtimes async em Rust. Features, I/O, timers e ecossistema. Qual escolher para seu projeto?

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ísticaTokioasync-std
Primeira versão20182019
MantenedorTokio Team (Alice Ryhl et al.)async-rs community
Modelo de execuçãoWork-stealing multi-threadWork-stealing multi-thread
Single-thread modecurrent_threadNão oficial
Timertokio::timeasync_std::task::sleep
Channelstokio::sync::mpsc, broadcast, watch, oneshotasync_std::channel
I/Otokio::io, tokio::net, tokio::fsasync_std::io, async_std::net, async_std::fs
EcossistemaImenso (axum, reqwest, tonic, sqlx…)Limitado (tide, surf…)
Downloads mensais~50M+~10M+
AtividadeMuito ativaManutençã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):

RuntimeConexões simultâneasThroughput
Tokio10.000~950K msg/s
async-std10.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.

Veja Também