Concorrência em Rust: de mutexes a async/await
Quando se fala em sistemas de alto desempenho, Rust instantaneamente se torna o centro das atenções. Mas o que o torna verdadeiramente único? É a concorrência em Rust — uma abordagem onde você obtém a velocidade do C e a segurança de memória, sem sacrificar a legibilidade do código. Neste artigo, vamos analisar as três principais ferramentas: threads clássicas, goroutines leves (no contexto do Rust, tarefas assíncronas) e o moderno async/await. Vamos lá!
Por que Rust não tem medo da concorrência?
Ao contrário de muitas linguagens, o Rust resolve o problema de condições de corrida (data races) no nível do compilador. O sistema de ownership (propriedade) e borrowing (empréstimo) garante que você não conseguirá modificar dados acidentalmente a partir de duas threads simultaneamente. Isso não é mágica — é uma verificação estática rigorosa. É por isso que o multithreading em Rust é considerado um dos mais seguros.
1. Threads (std::thread)
As threads clássicas do SO são a base. Em Rust, elas são implementadas através do módulo std::thread. Cada thread é um processo leve separado que pode ser executado em paralelo em diferentes núcleos da CPU.
Exemplo: iniciando threads
use std::thread;use std::time::Duration;
fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("Thread: {}", i); thread::sleep(Duration::from_millis(1)); } });
for i in 1..5 { println!("Thread principal: {}", i); thread::sleep(Duration::from_millis(1)); }
handle.join().unwrap();}Aqui, thread::spawn cria uma nova thread, e join aguarda sua conclusão. Simples, mas há uma nuance: as threads são "pesadas". Cada thread consome pelo menos 2-8 MB de memória de pilha. Para milhares de tarefas simultâneas, isso não é adequado.
Quando usar threads?
- Quando é necessário paralelismo real (tarefas CPU-bound).
- Quando o número de threads não excede o número de núcleos * 2.
- Quando você trabalha com operações bloqueantes (leitura de arquivos, chamadas síncronas).
2. Goroutines em Rust: mito ou realidade?
Estritamente falando, Rust não possui goroutines no mesmo sentido que Go. Mas possui tarefas assíncronas (tasks), que se comportam quase da mesma forma: são leves, alternam cooperativamente (sem envolvimento do SO) e podem ser executadas aos milhares em uma única thread. Para isso, utiliza-se o runtime Tokio ou async-std.
Exemplo: tarefa assíncrona (goroutine) via Tokio
use tokio::task;
#[tokio::main]async fn main() { let handle = task::spawn(async { for i in 1..10 { println!("Goroutine: {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; } });
for i in 1..5 { println!("Principal: {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; }
handle.await.unwrap();}Observe: task::spawn cria uma tarefa leve, que é executada dentro do pool de threads do Tokio. O custo de memória é de cerca de 2-3 KB por tarefa. Isso permite executar 100.000 "goroutines" sem problemas. Ao contrário do Go, aqui não há um escalonador embutido — você escolhe o runtime.
Como as goroutines do Rust diferem das do Go?
- Em Rust — as tarefas são cooperativas: elas próprias cedem o controle através de
.await. - Em Go — as goroutines são preemptivas: o escalonador as interrompe em pontos de E/S.
- Rust oferece mais controle: você pode escolher um runtime single-thread ou multi-thread.
3. Async/await: o coração do Rust assíncrono
As palavras-chave async e .await transformam código síncrono em assíncrono sem