Tasks, spawn e JoinHandle no Tokio
1. Introdução às Tasks no Tokio
No ecossistema Rust, o Tokio é um runtime assíncrono que permite escrever código concorrente de forma eficiente. Uma task no Tokio é uma unidade leve de execução agendada pelo runtime, similar a uma thread, mas muito mais barata em termos de recursos. Enquanto threads do sistema operacional consomem megabytes de memória cada, tasks Tokio ocupam apenas alguns kilobytes.
A diferença fundamental é que tasks são gerenciadas pelo próprio runtime, que utiliza um agendador cooperativo para alternar entre elas em pontos de .await. Isso permite executar milhares de tasks concorrentemente sem o overhead de threads reais.
// Uma task Tokio é essencialmente uma future que será executada
// concorrentemente com outras tasks no mesmo runtime.
use tokio;
#[tokio::main]
async fn main() {
println!("Runtime Tokio iniciado!");
}
2. A função tokio::spawn
A função tokio::spawn é a porta de entrada para criar tasks. Ela recebe uma Future e retorna um JoinHandle. Sua assinatura simplificada é:
pub fn spawn<T>(future: T) -> JoinHandle<T::Output>
where
T: Future + Send + 'static,
T::Output: Send + 'static,
Vamos criar nossa primeira task:
use tokio;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
println!("Executando dentro de uma task!");
42
});
// Erro comum: esquecer de .await o handle
let resultado = handle.await.unwrap();
println!("Resultado: {}", resultado);
}
Erro comum: Se você esquecer de .await o JoinHandle, a task ainda será executada (pois foi spawnada), mas você perderá o resultado e não terá garantia de que ela terminou.
3. O tipo JoinHandle
JoinHandle é como um "recibo" que permite interagir com a task spawnada. Seus métodos principais são:
use tokio;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
"Task concluída"
});
// Verificar se a task terminou
println!("Task finalizada? {}", handle.is_finished());
// Aguardar o resultado
let resultado = handle.await.unwrap();
println!("{}", resultado);
}
O método abort() cancela a task e será abordado na seção 6.
4. Propagação de erros com JoinHandle
O JoinHandle retorna um Result<T, JoinError>. Isso permite tratar tanto erros normais quanto panics dentro da task:
use tokio::task::JoinError;
#[tokio::main]
async fn main() {
// Task que pode panicar
let handle1 = tokio::spawn(async {
panic!("Algo deu errado!");
});
// Task que retorna erro
let handle2 = tokio::spawn(async {
Err::<i32, _>("Erro proposital")
});
// Tratamento robusto
match handle1.await {
Ok(_) => println!("Task 1 OK"),
Err(e) => {
if e.is_panic() {
let panic_msg = e.into_panic().downcast_ref::<&str>().unwrap();
println!("Task 1 panickou: {}", panic_msg);
}
}
}
// Usando unwrap_or_else
let resultado = handle2.await.unwrap_or_else(|e| {
eprintln!("Erro na task 2: {:?}", e);
0
});
}
5. Múltiplas tasks e concorrência
Podemos spawnar várias tasks e coletar seus resultados de forma eficiente:
use tokio;
#[tokio::main]
async fn main() {
let mut handles = Vec::new();
for i in 0..5 {
let handle = tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(100 * i)).await;
format!("Task {} concluída", i)
});
handles.push(handle);
}
// Coletar resultados sequencialmente
for handle in handles {
println!("{}", handle.await.unwrap());
}
}
Para maior eficiência, use join! ou try_join!:
use tokio;
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async { 10 + 20 });
let task2 = tokio::spawn(async { 30 + 40 });
let task3 = tokio::spawn(async { 50 + 60 });
// join! espera todas as futures, mas aqui usamos os JoinHandles
let (r1, r2, r3) = tokio::join!(task1, task2, task3);
println!("Resultados: {}, {}, {}", r1.unwrap(), r2.unwrap(), r3.unwrap());
}
6. Cancelamento de tasks
O cancelamento manual é feito com abort():
use tokio;
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
loop {
println!("Task rodando...");
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
});
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
// Cancelar a task
handle.abort();
// Verificar o erro de cancelamento
match handle.await {
Err(e) if e.is_cancelled() => println!("Task cancelada com sucesso"),
_ => println!("Task finalizou normalmente"),
}
}
Boas práticas: Prefira usar mecanismos cooperativos como canais (tokio::sync::watch) para sinalizar cancelamento, em vez de abort(), que é mais abrupto.
7. Tasks e ownership (propriedade)
Tasks spawnadas exigem 'static lifetime. Para mover dados para dentro de uma task, use closures move:
use std::sync::Arc;
use tokio;
#[tokio::main]
async fn main() {
let dados = vec![1, 2, 3, 4, 5];
let dados_compartilhados = Arc::new(dados);
let mut handles = Vec::new();
for i in 0..3 {
let dados_clone = dados_compartilhados.clone();
let handle = tokio::spawn(async move {
// Cada task tem sua própria referência aos dados
println!("Task {}: {:?}", i, *dados_clone);
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
}
Para tipos não-Send, você precisará usar tokio::task::spawn_local com um LocalSet.
8. Boas práticas e armadilhas comuns
Tasks órfãs: Cuidado com tasks que nunca terminam:
// RUIM: Task órfã que nunca é aguardada
tokio::spawn(async {
loop {
// Faz algo importante
}
});
// O programa pode terminar antes da task
// BOM: Aguardar explicitamente ou usar estruturas adequadas
let handle = tokio::spawn(async {
loop {
// Faz algo importante
}
});
// handle.await; // Aguardar quando apropriado
Limite de tasks: Para tarefas CPU-bound, use tokio::task::spawn_blocking ou pools de threads dedicadas. Tasks Tokio são ideais para operações I/O-bound.
Exemplo final - servidor simples:
use tokio::net::TcpListener;
#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
loop {
let (socket, addr) = listener.accept().await.unwrap();
// Spawna uma task para cada conexão
tokio::spawn(async move {
println!("Nova conexão de: {}", addr);
// Processa a conexão...
drop(socket);
});
}
}
Este padrão é extremamente eficiente: cada conexão recebe sua própria task leve, permitindo que o servidor lide com milhares de conexões simultâneas usando poucos threads do sistema.
Referências
- Tokio Documentation: Tasks — Documentação oficial sobre tasks, spawn e JoinHandle no Tokio
- Tokio Tutorial: Spawning — Tutorial oficial do Tokio explicando como spawnar tasks e gerenciar concorrência
- Rust Async Book: Tasks — Capítulo sobre múltiplas futures e tasks no livro oficial de async Rust
- Tokio Source Code: JoinHandle — Implementação do JoinHandle no repositório oficial do Tokio
- The Rust Programming Language: Fearless Concurrency — Capítulo sobre concorrência no livro oficial de Rust, com conceitos fundamentais aplicáveis ao Tokio