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