O Tokio é um runtime assíncrono para Rust, essencial para construir aplicações de alta performance e concorrência. Nesta aula, vamos explorar os conceitos fundamentais do Tokio na prática: como configurar o runtime, criar tarefas assíncronas com tokio::spawn e realizar operações de I/O assíncrono. Você verá exemplos funcionais e aprenderá a estruturar programas assíncronos de forma eficiente.

Dominar o Tokio é crucial para desenvolver servidores web, clientes de rede, sistemas distribuídos e qualquer aplicação que exija escalabilidade e baixa latência. Vamos começar entendendo o que é o runtime e como ele gerencia a execução de tarefas assíncronas.

Runtime

O runtime do Tokio é o coração do ecossistema assíncrono. Ele gerencia o agendamento de tarefas, o I/O e os timers, permitindo que você escreva código assíncrono como se fosse síncrono. Para usar o Tokio, você precisa ativá-lo com a macro #[tokio::main] ou construir um runtime manualmente.

A macro #[tokio::main] é a forma mais simples: ela transforma uma função async fn main() em um executável que inicializa o runtime e executa o corpo da função. Por baixo dos panos, ela chama Runtime::new() e block_on. Você também pode configurar o runtime com opções específicas, como número de threads, usando Runtime::builder().

Exemplo de configuração manual:

use tokio::runtime::Builder;

fn main() {
    let runtime = Builder::new_multi_thread()
        .worker_threads(4)
        .enable_all()
        .build()
        .unwrap();

    runtime.block_on(async {
        println!("Olá do Tokio!");
    });
}

O runtime multi-thread usa várias threads para executar tarefas, aproveitando múltiplos núcleos. O método enable_all() ativa I/O e timers. Você pode usar new_current_thread() para um runtime single-thread, útil para aplicações com pouca carga.

tokio::spawn

A função tokio::spawn é usada para criar uma nova tarefa assíncrona que será executada de forma concorrente. Cada tarefa é um future que o runtime agenda. spawn retorna um JoinHandle, que pode ser usado para aguardar a conclusão da tarefa ou cancelá-la.

As tarefas são agendadas no runtime, mas não garantem paralelismo: se você usar um runtime single-thread, elas serão executadas de forma cooperativa, alternando em pontos de await. Com multi-thread, elas podem rodar em paralelo.

Exemplo de spawn:

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("Tarefa iniciada");
        // simula trabalho
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        println!("Tarefa concluída");
    });

    println!("Aguardando tarefa");
    handle.await.unwrap();
    println!("Tarefa finalizada");
}

O JoinHandle implementa Future e retorna o resultado da tarefa. Se a tarefa entrar em pânico, o resultado é um Err.

Tarefas

No Tokio, uma tarefa é uma unidade de trabalho assíncrono, criada com spawn. Cada tarefa é executada de forma independente e pode se comunicar com outras tarefas através de canais, Mutex ou outros primitivos de sincronização.

É importante entender que tarefas são leves: cada uma ocupa pouca memória (poucos kilobytes), permitindo criar milhares delas. Mas cuidado: tarefas que realizam operações bloqueantes (como I/O síncrono) devem ser evitadas, pois bloqueiam a thread do runtime.

Para tarefas que precisam realizar trabalho pesado de CPU, use tokio::task::spawn_blocking, que envia a tarefa para um thread pool dedicado.

// Exemplo de spawn_blocking
let result = tokio::task::spawn_blocking(|| {
    // trabalho pesado de CPU
    let mut sum = 0;
    for i in 0..1_000_000 {
        sum += i;
    }
    sum
})
.await
.unwrap();
println!("Soma: {}", result);

Outra forma de criar tarefas é usar tokio::task::spawn_local para tarefas que não são Send, mas isso é avançado.

I/O assíncrono básico

O Tokio fornece primitivas de I/O assíncrono para arquivos, redes e processos. O mais comum é o uso de TcpStream e TcpListener para comunicação via TCP. Também há suporte para UDP, Unix sockets e timers.

Para leitura e escrita assíncrona, você usa os métodos read e write, que são futures. Exemplo de um servidor TCP simples:

use tokio::net::{TcpListener, TcpStream};
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 ouvindo em 127.0.0.1:8080");

    loop {
        let (mut socket, addr) = listener.accept().await?;
        println!("Conexão de {}", addr);
        tokio::spawn(async move {
            let mut buf = [0; 1024];
            loop {
                let n = match socket.read(&mut buf).await {
                    Ok(n) if n == 0 => break,
                    Ok(n) => n,
                    Err(e) => {
                        eprintln!("Erro ao ler: {}", e);
                        break;
                    }
                };
                if let Err(e) = socket.write_all(&buf[..n]).await {
                    eprintln!("Erro ao escrever: {}", e);
                    break;
                }
            }
        });
    }
}

Outro exemplo é o I/O de arquivos, usando tokio::fs::File:

use tokio::fs::File;
use tokio::io::AsyncReadExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut file = File::open("Cargo.toml").await?;
    let mut contents = String::new();
    file.read_to_string(&mut contents).await?;
    println!("Conteúdo: {}", contents);
    Ok(())
}

Boas práticas

  • Evite operações bloqueantes em tarefas assíncronas; use spawn_blocking.
  • Use #[tokio::main] para simplificar a inicialização, a menos que precise de configuração avançada.
  • Prefira tokio::io e tokio::net para I/O assíncrono.
  • Trate erros adequadamente: use Result e propagação de erros.
  • Para aplicações com muitas tarefas, considere usar tokio::sync para comunicação segura.

Referências

Exercícios

  1. Escreva um programa que use tokio::spawn para executar três tarefas que imprimam mensagens em ordem aleatória. Use JoinHandle para aguardar todas.
  2. ✓ Resposta:
    #[tokio::main]
    async fn main() {
        let handles: Vec<_> = (0..3).map(|i| {
            tokio::spawn(async move {
                println!("Tarefa {}", i);
            })
        }).collect();
    
        for handle in handles {
            handle.await.unwrap();
        }
    }
  3. Modifique o exemplo do servidor TCP para ecoar a mensagem em maiúsculas antes de enviar de volta.
  4. ✓ Resposta:
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::{TcpListener, TcpStream};
    
    #[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, addr) = listener.accept().await?;
            tokio::spawn(async move {
                let mut buf = [0; 1024];
                loop {
                    let n = socket.read(&mut buf).await?;
                    if n == 0 { break; }
                    let up = buf[..n].to_ascii_uppercase();
                    socket.write_all(&up).await?;
                }
            });
        }
    }
  5. Escreva uma função assíncrona que leia um arquivo e retorne o número de linhas. Use tokio::fs::File e AsyncReadExt.
  6. ✓ Resposta:
    use tokio::fs::File;
    use tokio::io::AsyncReadExt;
    
    async fn count_lines(path: &str) -> Result<usize, Box<dyn std::error::Error>> {
        let mut file = File::open(path).await?;
        let mut contents = String::new();
        file.read_to_string(&mut contents).await?;
        Ok(contents.lines().count())
    }
    
    #[tokio::main]
    async fn main() {
        let lines = count_lines("Cargo.toml").await.unwrap();
        println!("Linhas: {}", lines);
    }
  7. Crie um programa que use tokio::time::sleep para aguardar 2 segundos e depois imprima "Concluído".
  8. ✓ Resposta:
    #[tokio::main]
    async fn main() {
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        println!("Concluído");
    }
  9. Explique a diferença entre tokio::spawn e tokio::task::spawn_blocking e dê um exemplo de quando usar cada um.
  10. ✓ Resposta: tokio::spawn é para tarefas assíncronas que não bloqueiam; spawn_blocking é para tarefas síncronas que podem bloquear a thread do runtime. Use spawn para I/O assíncrono e uso de CPU leve; use spawn_blocking para operações de CPU pesadas ou I/O síncrono (como leitura de arquivo com std).