Threads — spawn, join e Send + Sync
Domine threads de SO em Rust: spawn, join, move closures, threads com escopo e os traits Send + Sync para liberdade de data races
Threads — spawn, join e Send + Sync
Rust fornece threads de SO através de std::thread. Combinado com o sistema de ownership, garante liberdade de data races em tempo de compilação — nenhuma outra linguagem oferece isso sem runtime ou garbage collector.
Threads Básicas
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("child: {i}");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("main: {i}");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap(); // Aguarda a child terminar
}Quando a thread principal encerra, todas as threads filhas são terminadas independentemente do estado. Sempre use join() se precisar que elas completem.
API de JoinHandle
use std::thread;
fn main() {
let handle: thread::JoinHandle<i32> = thread::spawn(|| {
// Faz o trabalho
42 // Valor de retorno da thread
});
// Bloqueia até a thread terminar, obtém o resultado
match handle.join() {
Ok(result) => println!("Thread returned: {result}"),
Err(e) => eprintln!("Thread panicked: {:?}", e),
}
// Verificação não-bloqueante (thread é destacada se dropped)
let handle = thread::spawn(|| {});
let is_finished = handle.is_finished(); // Verifica sem bloquear
}Move Closures com Threads
Threads podem sobreviver ao escopo que as criou. A palavra-chave move transfere a propriedade:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// ERROR: closure pode sobreviver à função envolvente
// thread::spawn(|| {
// println!("{:?}", v);
// });
// CORREÇÃO: mover propriedade para a thread
let handle = thread::spawn(move || {
println!("{:?}", v);
});
handle.join().unwrap();
// println!("{:?}", v); // ERROR: v foi movido
}move força o closure a assumir propriedade de todas as variáveis capturadas. Se precisar compartilhar acesso (não mover), use Arc.
Move Parcial com Threads
use std::thread;
fn main() {
let name = String::from("worker");
let data = vec![1, 2, 3];
// Não pode mover seletivamente — todas as variáveis capturadas são movidas
let handle = thread::spawn(move || {
println!("{name} processing: {:?}", data);
});
handle.join().unwrap();
}Construtor de Thread — Personalizando Threads
use std::thread;
fn main() {
let builder = thread::Builder::new()
.name("worker-1".into())
.stack_size(1024 * 1024); // Pilha de 1 MB
let handle = builder.spawn(|| {
println!("Running in: {:?}", thread::current().name());
42
}).unwrap();
println!("Result: {}", handle.join().unwrap());
}| Método do Builder | Propósito |
|---|---|
.name(name) | Definir nome da thread (útil para depuração) |
.stack_size(size) | Definir tamanho da pilha (padrão: 2MB) |
.spawn(f) | Criar a thread |
Threads com Escopo (Scoped Threads)
thread::scope permite emprestar dados sem move:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let mut results = vec![];
// Threads com escopo podem emprestar da thread pai
thread::scope(|s| {
s.spawn(|| {
// Pode emprestar v sem move
results.push(v.iter().sum::<i32>());
});
s.spawn(|| {
results.push(v.iter().product::<i32>());
});
}); // Todas as threads são unidas aqui
println!("{results:?}"); // [6, 6]
println!("{v:?}"); // v ainda acessível
}Comunicação entre Threads — Padrões Básicos
use std::thread;
fn main() {
// Computar em paralelo, coletar resultados
let mut handles = vec![];
for i in 0..5 {
let handle = thread::spawn(move || {
i * i
});
handles.push(handle);
}
let results: Vec<i32> = handles
.into_iter()
.map(|h| h.join().unwrap())
.collect();
println!("{results:?}"); // [0, 1, 4, 9, 16]
}Traits Send e Sync
Estes traits marcadores estão no coração da segurança de threads do Rust:
| Trait | Significado | Auto-implementado? |
|---|---|---|
Send | Ownership pode ser transferida entre threads | Sim (exceto Rc, raw pointers, etc.) |
Sync | Referência compartilhada &T pode ser enviada entre threads | Sim (exceto Cell, RefCell, etc.) |
use std::thread;
use std::rc::Rc;
fn main() {
// Rc NÃO é Send — não pode ser movido entre threads
// let rc = Rc::new(5);
// thread::spawn(move || { println!("{rc}"); });
// Arc É Send — pode ser movido entre threads
let arc = std::sync::Arc::new(5);
let arc_clone = arc.clone();
thread::spawn(move || {
println!("{}", arc_clone);
}).join().unwrap();
}Como o Compilador Reforça Segurança de Threads
use std::cell::Cell;
use std::thread;
struct NotSync {
cell: Cell<i32>,
}
// NotSync é !Sync porque Cell é !Sync
fn main() {
let ns = NotSync { cell: Cell::new(42) };
// ERROR: NotSync não pode ser compartilhado entre threads com segurança
// thread::scope(|s| {
// s.spawn(|| {
// println!("{}", ns.cell.get());
// });
// });
}Rc<T> | Não | Não | Contagem de ref não-atômica |
| Arc<T> | Sim | Sim | Contagem de ref atômica |
| Cell<T> | Sim | Não | Mutabilidade interior, sem sync |
| RefCell<T> | Sim | Não | Mutabilidade interior, sem sync |
| Mutex<T> | Sim | Sim | Fornece Sync |
| `AtomicBool` | Sim | Sim | Operações atômicas |
Armazenamento Local de Thread
use std::cell::RefCell;
thread_local! {
static COUNTER: RefCell<u32> = RefCell::new(0);
}
fn main() {
// Cada thread tem seu próprio COUNTER
let h1 = std::thread::spawn(|| {
COUNTER.with(|c| {
*c.borrow_mut() = 42;
println!("Thread 1: {}", c.borrow());
});
});
let h2 = std::thread::spawn(|| {
COUNTER.with(|c| {
println!("Thread 2: {}", c.borrow()); // 0, não 42
});
});
h1.join().unwrap();
h2.join().unwrap();
}Exemplo Real: Processamento de Imagem em Paralelo
use std::thread;
fn process_pixel(pixel: u8) -> u8 {
// Simula computação cara
pixel.saturating_mul(2)
}
fn process_image_parallel(image: Vec<u8>, num_threads: usize) -> Vec<u8> {
let chunk_size = (image.len() + num_threads - 1) / num_threads;
let mut handles = vec![];
for chunk in image.chunks(chunk_size) {
let chunk = chunk.to_vec();
handles.push(thread::spawn(move || {
chunk.into_iter().map(process_pixel).collect::<Vec<_>>()
}));
}
let mut result = Vec::with_capacity(image.len());
for handle in handles {
result.extend(handle.join().unwrap());
}
result
}
fn main() {
let image: Vec<u8> = (0..100).collect();
let processed = process_image_parallel(image.clone(), 4);
println!("Original: {:?}", &image[..10]);
println!("Processed: {:?}", &processed[..10]);
}Perguntas de Prática
- Como criar uma thread e aguardar sua conclusão?
- Por que closures passadas para
thread::spawndevem usarmove? - Qual é a diferença entre
thread::spawne threads com escopo? - O que
handle.join()retorna? - O que são os traits
SendeSync? - Quais tipos padrão NÃO são
Send? Por quê? - O que são threads com escopo (
thread::scope) e quando são úteis? - Como personalizar o nome e tamanho da pilha de uma thread?
- Como o compilador previne data races em código com threads?
- O que é armazenamento local de thread e como é usado?