Concorrência e Multithreading em Rust: Escrevendo Código Paralelo Seguro
Rust é uma das poucas linguagens que resolve o problema de data races em tempo de compilação. Graças ao sistema de ownership e borrowing, o código concorrente em Rust se torna não apenas rápido, mas também seguro. Neste artigo, vamos abordar os fundamentos do multithreading em Rust: criação de threads, troca de dados através de canais e mutexes, além de conhecer o modelo "Fearless Concurrency".
1. Fundamentos do Multithreading: std::thread
Rust fornece suporte nativo a threads através do módulo std::thread. É possível criar uma nova thread usando a função thread::spawn, que recebe uma closure.
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(); // aguarda a conclusão da thread}O método join() bloqueia a thread principal até que a thread filha termine. Sem ele, o programa pode terminar antes que a thread seja executada.
2. Transferência de Dados entre Threads: Canais (mpsc)
Para troca de mensagens entre threads em Rust, utiliza-se o modelo multiple producer, single consumer (mpsc). Isso é implementado no módulo std::sync::mpsc.
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
thread::spawn(move || { let val = String::from("Olá da thread!"); tx.send(val).unwrap(); // val já foi movido, não pode mais ser usado });
let received = rx.recv().unwrap(); println!("Recebido: {}", received);}Observe a palavra-chave move na closure — ela transfere a ownership da variável tx para a nova thread. O método send() retorna um Result, que pode ser Err se o receptor foi fechado.
Para criar múltiplos produtores, pode-se clonar o tx:
let (tx, rx) = mpsc::channel();let tx1 = tx.clone();
thread::spawn(move || { tx.send("Mensagem 1").unwrap();});
thread::spawn(move || { tx1.send("Mensagem 2").unwrap();});
for received in rx { println!("Recebido: {}", received);}3. Memória Compartilhada: Mutex e Arc
Canais são convenientes para transferir dados, mas às vezes é necessário compartilhar estado entre threads. Para isso, usa-se Mutex (exclusão mútua) em conjunto com Arc (contagem atômica de referências).
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("Resultado: {}", *counter.lock().unwrap());}Por que Arc e não Rc? O Rc comum não é thread-safe, pois seu contador de referências não é atômico. Arc usa operações atômicas, tornando-o seguro para multithreading.
Importante: Mutex bloqueia a thread ao chamar lock(). Após sair do escopo, o bloqueio é automaticamente liberado (graças à trait Drop).