Introdução: por que precisamos de filas de tarefas em Rust?
Ao desenvolver aplicações em Rust, frequentemente surge a necessidade de processar tarefas de forma assíncrona ou distribuir a carga entre várias threads. Uma fila de tarefas (task queue) é uma estrutura de dados fundamental que permite organizar o processamento sequencial ou concorrente de unidades de trabalho. Neste artigo, vamos explorar como criar e usar filas de tarefas em Rust, começando com exemplos simples e avançando para cenários reais.
Rust oferece várias abordagens para implementar filas: desde canais padrão do std::sync::mpsc até soluções assíncronas avançadas baseadas em tokio. Vamos analisar cada uma delas e mostrar como escolher a ferramenta certa para sua tarefa.
Fundamentos: fila de tarefas com canais mpsc
A maneira mais simples de criar uma fila de tarefas em Rust é usar um canal multi-produtor, único-consumidor (mpsc) da biblioteca padrão. Esse mecanismo é ideal para situações em que uma thread processa tarefas enviadas por várias threads produtoras.
Criando uma fila simples
Vamos considerar um exemplo básico onde a thread principal envia tarefas e uma thread separada as processa:
use std::sync::mpsc;use std::thread;use std::time::Duration;
fn main() { // Criando o canal: transmissor e receptor let (tx, rx) = mpsc::channel::<String>();
// Iniciando a thread processadora let handler = thread::spawn(move || { for task in rx { println!("Processando tarefa: {}", task); thread::sleep(Duration::from_millis(500)); } });
// Enviando tarefas let tasks = vec!["Tarefa 1", "Tarefa 2", "Tarefa 3"]; for task in tasks { tx.send(task.to_string()).unwrap(); println!("Enviado: {}", task); }
// Fechando o canal para que o processador termine drop(tx); handler.join().unwrap();}Este código demonstra a mecânica básica: tx.send() coloca a tarefa na fila, e o receptor rx as extrai na ordem de chegada (FIFO). Observe que o canal bloqueia automaticamente o produtor se a fila estiver cheia, e bloqueia o consumidor se a fila estiver vazia.
Múltiplos produtores
Uma das principais vantagens do mpsc é a possibilidade de clonar o transmissor para várias threads:
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel::<u32>(); let mut handles = vec![];
// Iniciando 3 threads produtoras for id in 0..3 { let tx_clone = tx.clone(); handles.push(thread::spawn(move || { for i in 0..5 { let task = id * 100 + i; tx_clone.send(task).unwrap(); } })); }
// Fechando o transmissor original drop(tx);
// Processando todas as tarefas for received in rx { println!("Tarefa recebida: {}", received); }
for handle in handles { handle.join().unwrap(); }}Cada produtor envia suas tarefas, e um único consumidor as processa na ordem de chegada. Este é o padrão clássico "produtor-consumidor".
Fila avançada com prioridades
O canal mpsc padrão não suporta prioridades — as tarefas são processadas estritamente na ordem de envio. No entanto, podemos implementar uma fila com prioridades usando BinaryHeap da biblioteca padrão.
Implementação de fila prioritária
use std::collections::BinaryHeap;use std::cmp::Ordering;
#[derive(Debug, Eq, PartialEq)]struct Task { priority: u8, data: String,}
impl Ord for Task { fn cmp(&self, other: &Self) -> Ordering { // Quanto menor o número, maior a prioridade other.priority.cmp(&self