Tokio na prática
Nesta aula, você aprenderá a usar o Tokio na prática, desde a configuração do runtime até a criação de tarefas assíncronas e operações de I/O, com exemplos reais e exercícios para fixar o conteúdo.
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::ioetokio::netpara I/O assíncrono. - Trate erros adequadamente: use
Resulte propagação de erros. - Para aplicações com muitas tarefas, considere usar
tokio::syncpara comunicação segura.
Referências
- Tokio Tutorial
- Documentação do Tokio (docs.rs)
- Documentação do Runtime
- tokio::spawn
- TcpListener
- Arquivo assíncrono
Exercícios
- Escreva um programa que use
tokio::spawnpara executar três tarefas que imprimam mensagens em ordem aleatória. UseJoinHandlepara aguardar todas. - Modifique o exemplo do servidor TCP para ecoar a mensagem em maiúsculas antes de enviar de volta.
- Escreva uma função assíncrona que leia um arquivo e retorne o número de linhas. Use
tokio::fs::FileeAsyncReadExt. - Crie um programa que use
tokio::time::sleeppara aguardar 2 segundos e depois imprima "Concluído". - Explique a diferença entre
tokio::spawnetokio::task::spawn_blockinge dê um exemplo de quando usar cada um.
#[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();
}
}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?;
}
});
}
}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);
}#[tokio::main]
async fn main() {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
println!("Concluído");
}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).