Threads
Esta aula aborda a criação e gerenciamento de threads em Rust, incluindo spawn, join, o uso de move em closures, compartilhamento de dados com Arc<Mutex<T>> e os riscos de concorrência como deadlocks e data races. O objetivo é capacitar o aluno a escrever programas concorrentes seguros e eficientes.
Threads são a unidade básica de concorrência em sistemas operacionais. Em Rust, a biblioteca padrão fornece suporte a threads nativas do sistema operacional através do módulo std::thread. Nesta aula, vamos explorar como criar threads, esperar sua conclusão, compartilhar dados entre elas e evitar os perigos comuns da programação concorrente.
Diferente de linguagens como C ou Java, Rust oferece garantias de segurança em tempo de compilação para evitar condições de corrida e data races. Entender como usar threads corretamente é essencial para escrever programas paralelos performáticos e seguros.
spawn e join
Para criar uma nova thread, usamos thread::spawn, que recebe uma closure contendo o código a ser executado na nova thread. A função retorna um JoinHandle, que permite esperar a thread terminar através do método join(). Sem join(), a thread principal pode terminar antes da thread filha, abortando-a.
Exemplo básico:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("Thread filha: {}", 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 thread filha terminar
}Note que se não chamarmos join(), a thread principal pode terminar antes da filha, e o programa encerrará a thread filha abruptamente. O método join() retorna um Result; usamos unwrap() para simplificar, mas em código real devemos tratar erros.
move em closures de thread
Closures passadas para thread::spawn podem capturar variáveis do escopo externo. No entanto, a closure pode precisar tomar posse dos valores capturados, especialmente se a thread viver mais que a variável original. Para isso, usamos a palavra-chave move antes da closure, forçando a transferência de ownership para a closure.
Exemplo sem move (não compila):
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Vetor: {:?}", v); // erro: v pode ser droppado antes
});
drop(v); // erro: v não pode ser movido para a closure e usado aqui
handle.join().unwrap();
}O compilador reclama que a closure pode sobreviver à variável v. Adicionando move, transferimos a posse de v para a thread:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Vetor: {:?}", v); // v é movido para a closure
});
// drop(v); // agora não podemos mais usar v aqui, pois foi movido
handle.join().unwrap();
}Isso garante que a thread tenha ownership dos dados, evitando referências pendentes.
Compartilhando dados
Para compartilhar dados entre múltiplas threads, Rust oferece primitivas como Arc (Atomic Reference Counting) e Mutex (exclusão mútua). Arc permite que múltiplas threads possuam ownership compartilhado, enquanto Mutex garante que apenas uma thread acesse os dados por vez.
Exemplo com Arc<Mutex<T>>:
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());
}Note que clonamos o Arc para cada thread (incrementa a contagem de referências) e usamos lock() para acessar o mutex. O MutexGuard retornado implementa Deref para acessar o valor interno.
Riscos
Programação concorrente introduz riscos como:
- Deadlock: duas threads esperam uma pela outra liberar um recurso. Em Rust, deadlocks podem ocorrer com múltiplos mutexes se não houver uma ordem consistente de lock.
- Data races: acesso concorrente não sincronizado a dados mutáveis. Rust previne data races em tempo de compilação através do sistema de ownership e borrow checking, mas ainda é possível com código inseguro (
unsafe). - Panics em threads: um panic em uma thread não afeta outras; a thread principal deve verificar o resultado de
join()para detectar panics. - Overhead de threads: criar muitas threads pode degradar performance. Considere usar um thread pool (como
rayon) para tarefas leves.
Boas práticas: minimize o escopo de locks, evite locks aninhados, e prefira std::sync::mpsc (canais) para comunicação entre threads, que é mais segura.
Referências
- The Rust Programming Language - Threads
- std::thread documentation
- Arc documentation
- Mutex documentation
- The Rustonomicon - Concurrency
Exercícios
Crie um programa que lance 5 threads, cada uma imprimindo seu número (0 a 4) e depois durma 1 segundo. A thread principal deve esperar todas terminarem.
✓ Resposta:use std::thread; use std::time::Duration; fn main() { let mut handles = vec![]; for i in 0..5 { let handle = thread::spawn(move || { println!("Thread {}", i); thread::sleep(Duration::from_secs(1)); }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } }Explique por que o código abaixo não compila e corrija-o usando
move.use std::thread; fn main() { let msg = String::from("Olá"); let handle = thread::spawn(|| { println!("{}", msg); }); drop(msg); handle.join().unwrap(); }✓ Resposta:O código não compila porque a closure capturamsgpor referência, mas depoisdrop(msg)tenta movermsgpara fora, causando conflito de ownership. Além disso, a thread pode executar após o drop. A correção é usarmovena closure, transferindo ownership demsgpara a thread, e remover odrop(ou mantê-lo, mas não funcionará poismsgfoi movido). Código corrigido:use std::thread; fn main() { let msg = String::from("Olá"); let handle = thread::spawn(move || { println!("{}", msg); }); // drop(msg); // msg foi movido, não pode mais ser usado handle.join().unwrap(); }Escreva um programa que compartilhe um vetor de inteiros entre duas threads: uma thread adiciona números pares, a outra ímpares. Use
ArceMutex.✓ Resposta:use std::sync::{Arc, Mutex}; use std::thread; fn main() { let numbers = Arc::new(Mutex::new(Vec::new())); let mut handles = vec![]; // Thread para pares let nums = Arc::clone(&numbers); let handle = thread::spawn(move || { let mut data = nums.lock().unwrap(); for i in (0..10).step_by(2) { data.push(i); } }); handles.push(handle); // Thread para ímpares let nums = Arc::clone(&numbers); let handle = thread::spawn(move || { let mut data = nums.lock().unwrap(); for i in (1..10).step_by(2) { data.push(i); } }); handles.push(handle); for handle in handles { handle.join().unwrap(); } let result = numbers.lock().unwrap(); println!("Vetor: {:?}", *result); }O que é um deadlock? Dê um exemplo em Rust com dois mutexes e mostre como evitá-lo.
✓ Resposta:Deadlock é uma situação onde duas ou mais threads ficam bloqueadas permanentemente, cada uma esperando por um recurso que a outra detém. Exemplo de deadlock:
Para evitar, adote uma ordem global de lock (ex.: sempre travar m1 antes de m2).use std::sync::{Arc, Mutex}; use std::thread; fn main() { let m1 = Arc::new(Mutex::new(())); let m2 = Arc::new(Mutex::new(())); let m1_clone = Arc::clone(&m1); let m2_clone = Arc::clone(&m2); let handle1 = thread::spawn(move || { let _lock1 = m1_clone.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _lock2 = m2_clone.lock().unwrap(); }); let handle2 = thread::spawn(move || { let _lock2 = m2.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _lock1 = m1.lock().unwrap(); }); handle1.join().unwrap(); handle2.join().unwrap(); }Usando canais (
std::sync::mpsc), crie um programa onde a thread principal envia uma string para uma thread filha, que a imprime em maiúsculas e envia de volta. A thread principal então imprime a resposta.✓ Resposta:use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); let (tx_back, rx_back) = mpsc::channel(); let handle = thread::spawn(move || { let received = rx.recv().unwrap(); let response = received.to_uppercase(); tx_back.send(response).unwrap(); }); tx.send("hello world".to_string()).unwrap(); let response = rx_back.recv().unwrap(); println!("Resposta: {}", response); handle.join().unwrap(); }