Noções Básicas de Concorrência
9 exemplos para você começar com concorrência em Rust - 6 básicos e 3 intermediários.
Pré-requisitos
- Rust 1.97.0 (edição 2024) com
std::thread. - Entenda propriedade (ownership) e closures
moveantes de criar threads.
Exemplos Básicos
1. Criando uma Thread
std::thread::spawn executa uma closure em uma nova thread do sistema operacional.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("olá de uma thread");
});
handle.join().unwrap();
}spawnaceita uma closure'static- sem dados emprestados da pilha, a menos que sejam escopados (veja threads escopadas).joinbloqueia até que a thread termine e retornaResult<T, JoinError>.- A thread principal espera aqui; sem
join, o processo pode sair antes que a filha seja executada.
Relacionado: Threads Escopadas - empreste dados da pilha com segurança
2. Movendo Dados para uma Thread
Use move para transferir a propriedade através das fronteiras da thread.
use std::thread;
fn main() {
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("dados de propriedade: {:?}", data);
});
handle.join().unwrap();
}moveforça a closure a assumir a propriedade das variáveis capturadas.- Após
move,datanão está disponível na thread pai. - Tipos movidos para threads devem implementar
Send.
Relacionado: Send & Sync - marcadores de segurança de thread
3. JoinHandle e Valores de Retorno
Threads podem retornar valores através de join.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
let sum: i32 = (1..=100).sum();
sum
});
let result = handle.join().unwrap();
println!("soma = {result}");
}- O tipo de retorno da closure se torna
JoinHandle<T>. joinmove o resultado para fora da thread.- Panics na thread filha se propagam como
Errdejoin.
Relacionado: Deadlocks e Condições de Corrida - modos de falha
4. Múltiplas Threads
Crie vários workers e junte-os em ordem.
use std::thread;
fn main() {
let handles: Vec<_> = (0..4)
.map(|i| {
thread::spawn(move || {
println!("worker {i}");
i * 10
})
})
.collect();
for handle in handles {
let n = handle.join().unwrap();
println!("recebido {n}");
}
}- Colete os handles primeiro, depois junte - os workers rodam concorrentemente.
- A ordem de junção não afeta qual thread termina primeiro.
- Armazene os handles se precisar de resultados ou de um desligamento limpo.
Relacionado: Paralelismo de Dados com Rayon - iteradores paralelos
5. Nomes e IDs de Threads
Nomeie threads para logs e depuração mais claros.
use std::thread::{self, ThreadId};
fn main() {
let builder = thread::Builder::new().name("worker".into());
let handle = builder.spawn(|| {
let id: ThreadId = thread::current().id();
println!("{:?}", thread::current().name());
println!("id = {id:?}");
}).unwrap();
handle.join().unwrap();
}- Nomes aparecem em
tracing, depuradores e mensagens de pânico. ThreadIdé opaco e útil para correlacionar logs.- O builder também suporta substituições de tamanho de pilha para recursão profunda.
Relacionado: Melhores Práticas de Concorrência - regras operacionais
6. Passagem de Mensagens com Canais
Prefira canais em vez de estado mutável compartilhado quando possível.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("job done").unwrap();
});
let msg = rx.recv().unwrap();
println!("{msg}");
}mpsc= multiple producer, single consumer (múltiplos produtores, um consumidor).sendmove a propriedade da mensagem para o receptor.recvbloqueia até que uma mensagem chegue.
Relacionado: Canais - guia completo de canais
Exemplos Intermediários
7. Contador Compartilhado com Mutex
Ao compartilhar estado, envolva-o em um lock.
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);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("contagem = {}", *counter.lock().unwrap());
}Arccompartilha a propriedade entre threads;Mutexserializa a mutação.- Sempre libere o lock antes de fazer I/O lento (solte o guardião).
lock().unwrap()entra em pânico se o mutex for envenenado após um pânico em outra thread.
Relacionado: Mutex & RwLock - padrões de bloqueio
8. Cargas de Trabalho com Muitas Leituras com RwLock
Muitos leitores, poucos escritores se beneficiam de RwLock.
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let cache = Arc::new(RwLock::new(String::from("seed")));
let mut handles = vec![];
for i in 0..5 {
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let data = cache.read().unwrap();
println!("leitor {i}: {data}");
}));
}
{
let mut data = cache.write().unwrap();
data.push_str(" updated");
}
for handle in handles {
handle.join().unwrap();
}
}- Múltiplos guardiões
readpodem coexistir. writebloqueia todos os leitores e outros escritores.- Prefira canais se as escritas forem frequentes - a contenção de lock prejudica a taxa de transferência.
Relacionado: Arc para Estado Compartilhado - padrões de compartilhamento
9. Escolhendo Threads vs Async
Threads para paralelismo de CPU; async para muitas esperas de I/O concorrentes.
// Ligado à CPU: threads ou rayon
// use rayon::prelude::*;
// Ligado a I/O: runtime async (Tokio)
// #[tokio::main]
// async fn main() { ... }- Threads do SO têm sobrecarga de pilha (cerca de 2 MB por padrão); milhares de threads bloqueantes são caras.
- Tarefas async são leves, mas precisam de um runtime e I/O não bloqueante.
- Designs híbridos usam pools de threads dentro do async para trabalho de CPU (
spawn_blocking).
Relacionado: Noções Básicas de Async - quando usar async
Versões da Pilha: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4 e Polars 0.46+.