Rust

Concorrência — Threads, Canais e a Promessa de Rust de Concorrência Sem Medo Já leu

13 min de leitura

Concorrência — Threads, Canais e a Promessa de Rust de Concorrência Sem Medo
Concorrência é um dos territórios mais perigosos da programação. Data races, deadlocks, condições de corrida — esses bugs são notoriamente difíceis de reproduzir, diagnosticar e corrigir. Linguagens como C e C++ oferecem

 

Concorrência é um dos territórios mais perigosos da programação. Data races, deadlocks, condições de corrida — esses bugs são notoriamente difíceis de reproduzir, diagnosticar e corrigir. Linguagens como C e C++ oferecem threads poderosas mas sem garantias: o compilador não te avisa quando você está criando uma condição de corrida.

Rust faz uma promessa ousada: concorrência sem medo. O sistema de tipos — ownership, borrowing e os traits Send e Sync — torna impossível compilar código com data races. Não é uma questão de disciplina ou cuidado. É uma garantia do compilador.

Neste artigo vamos explorar threads nativas, comunicação por canais, e estado compartilhado seguro com Mutex e Arc.


Criando threads

Threads em Rust são criadas com std::thread::spawn. A closure passada para spawn é executada em uma nova thread do sistema operacional:

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Thread filha: {i}");
            thread::sleep(Duration::from_millis(50));
        }
    });

    for i in 1..=3 {
        println!("Thread principal: {i}");
        thread::sleep(Duration::from_millis(80));
    }

    // join() aguarda a thread filha terminar
    handle.join().unwrap();
    println!("Ambas as threads concluíram.");
}

Saída (ordem pode variar):

Thread principal: 1
Thread filha: 1
Thread filha: 2
Thread principal: 2
Thread filha: 3
Thread filha: 4
Thread principal: 3
Thread filha: 5
Ambas as threads concluíram.

thread::spawn retorna um JoinHandle — um handle para a thread. Chamar .join() bloqueia a thread atual até que a thread filha termine. Sem join, a thread principal pode encerrar antes da filha, matando-a prematuramente.


move closures em threads

Para usar dados da thread principal dentro de uma thread filha, a closure precisa tomar posse desses dados com move:

use std::thread;

fn main() {
    let dados = vec![1, 2, 3, 4, 5];

    // sem move: ERRO — compilador não sabe se dados
    // ainda existirá quando a thread rodar
    let handle = thread::spawn(move || {
        println!("Dados na thread filha: {:?}", dados);
        let soma: i32 = dados.iter().sum();
        println!("Soma: {soma}");
    });

    // dados foi movido para a thread — não pode mais ser usado aqui
    // println!("{:?}", dados); // ERRO de compilação

    handle.join().unwrap();
}

O move é obrigatório aqui — sem ele, o compilador recusa porque não pode garantir que dados viverá pelo menos tanto quanto a thread. Com move, a thread toma posse de dados e é responsável pela sua destruição.


Retornando valores de threads

Threads podem retornar valores através do JoinHandle:

use std::thread;

fn calcular_em_paralelo(dados: Vec<Vec<i64>>) -> Vec<i64> {
    let handles: Vec<_> = dados
        .into_iter()
        .map(|chunk| {
            thread::spawn(move || {
                chunk.iter().sum::<i64>()
            })
        })
        .collect();

    handles
        .into_iter()
        .map(|h| h.join().unwrap())
        .collect()
}

fn main() {
    let dados = vec![
        vec![1, 2, 3, 4, 5],
        vec![10, 20, 30, 40, 50],
        vec![100, 200, 300, 400, 500],
    ];

    let somas = calcular_em_paralelo(dados);
    println!("Somas parciais: {:?}", somas); // [15, 150, 1500]

    let total: i64 = somas.iter().sum();
    println!("Total: {total}"); // 1665
}

Canais — comunicação entre threads

A abordagem mais idiomática para comunicação entre threads em Rust é o modelo de canais — inspirado na filosofia Go de "não compartilhe memória para comunicar; comunique para compartilhar memória."

Um canal tem dois lados: um transmissor (Sender) e um receptor (Receiver). Dados fluem do transmissor para o receptor:

use std::sync::mpsc; // multiple producer, single consumer
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let mensagens = vec!["olá", "do", "outro", "lado"];
        for msg in mensagens {
            tx.send(msg).unwrap();
            println!("Enviado: {msg}");
        }
    });

    // rx.recv() bloqueia até receber uma mensagem
    for recebido in rx {
        println!("Recebido: {recebido}");
    }

    println!("Canal fechado.");
}

O loop for recebido in rx itera sobre o receptor até que o canal seja fechado — o que acontece automaticamente quando todos os transmissores são destruídos.

Múltiplos produtores

O mpsc suporta múltiplos transmissores através de clone:

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    // Três threads produtoras
    for id in 0..3 {
        let tx_clone = tx.clone();
        thread::spawn(move || {
            for i in 0..3 {
                let msg = format!("Thread {id} — mensagem {i}");
                tx_clone.send(msg).unwrap();
                thread::sleep(Duration::from_millis(10));
            }
        });
    }

    // Necessário: descartamos o tx original para que
    // o canal feche quando todas as cópias forem destruídas
    drop(tx);

    let mut total = 0;
    for msg in rx {
        println!("{msg}");
        total += 1;
    }

    println!("Total de mensagens recebidas: {total}"); // 9
}

O drop(tx) é crucial — sem ele, o canal nunca fecha, pois o transmissor original ainda existe na thread principal.


Estado compartilhado com Arc>

Canais são ótimos para comunicação unidirecional. Mas às vezes você precisa que múltiplas threads compartilhem e modifiquem o mesmo dado. Para isso, Rust oferece Arc>:

  • Arc — como Rc, mas thread-safe (Atomic Reference Counted)
  • Mutex — garante que apenas uma thread acessa o dado por vez
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let contador = Arc::new(Mutex::new(0i32));
    let mut handles = Vec::new();

    for _ in 0..10 {
        let contador_clone = Arc::clone(&contador);

        let handle = thread::spawn(move || {
            let mut valor = contador_clone.lock().unwrap();
            *valor += 1;
            // lock liberado automaticamente quando `valor` sai de escopo
        });

        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Contador final: {}", *contador.lock().unwrap()); // 10
}

lock() bloqueia a thread atual até que o mutex esteja disponível. Retorna um MutexGuard — um smart pointer que libera o lock automaticamente quando sai de escopo. Isso torna impossível esquecer de liberar o lock — um bug clássico em outras linguagens.


Um exemplo mais completo: pool de trabalho

Vamos implementar um pool de threads que processa tarefas em paralelo:

use std::sync::{Arc, Mutex};
use std::thread;

struct PoolDeTrabalho {
    handles: Vec<thread::JoinHandle<()>>,
    sender: std::sync::mpsc::Sender<Box<dyn FnOnce() + Send + 'static>>,
}

impl PoolDeTrabalho {
    fn novo(tamanho: usize) -> Self {
        let (tx, rx) = std::sync::mpsc::channel::<Box<dyn FnOnce() + Send + 'static>>();
        let rx = Arc::new(Mutex::new(rx));
        let mut handles = Vec::new();

        for id in 0..tamanho {
            let rx_clone = Arc::clone(&rx);
            let handle = thread::spawn(move || {
                loop {
                    let tarefa = {
                        let lock = rx_clone.lock().unwrap();
                        lock.recv()
                    };

                    match tarefa {
                        Ok(f) => {
                            println!("  Worker {id} processando...");
                            f();
                        }
                        Err(_) => {
                            println!("  Worker {id} encerrando.");
                            break;
                        }
                    }
                }
            });
            handles.push(handle);
        }

        PoolDeTrabalho { handles, sender: tx }
    }

    fn executar<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        self.sender.send(Box::new(f)).unwrap();
    }

    fn aguardar(self) {
        drop(self.sender); // fecha o canal
        for handle in self.handles {
            handle.join().unwrap();
        }
    }
}

fn main() {
    println!("Iniciando pool com 3 workers...
");
    let pool = PoolDeTrabalho::novo(3);

    let resultados = Arc::new(Mutex::new(Vec::new()));

    for i in 0..9 {
        let resultados_clone = Arc::clone(&resultados);
        pool.executar(move || {
            // Simula trabalho
            let resultado = i * i;
            thread::sleep(std::time::Duration::from_millis(50));
            resultados_clone.lock().unwrap().push((i, resultado));
        });
    }

    pool.aguardar();

    let mut res = resultados.lock().unwrap();
    res.sort_by_key(|&(i, _)| i);

    println!("
── Resultados ──");
    for (i, r) in res.iter() {
        println!("  {i}² = {r}");
    }
}

Os traits Send e Sync

A garantia de concorrência segura de Rust é implementada por dois traits especiais do compilador:

Send — um tipo que implementa Send pode ser transferido entre threads. Arc é Send. Rc não é — não seria seguro transferir um Rc para outra thread porque o contador de referências não é atômico.

Sync — um tipo que implementa Sync pode ser referenciado por múltiplas threads simultaneamente. Mutex é Sync. RefCell não é — suas verificações de borrow em runtime não são thread-safe.

Esses traits são implementados automaticamente pelo compilador para tipos que satisfazem as condições. Se você tentar enviar um Rc para outra thread, o compilador recusa com uma mensagem clara explicando que Rc não é Send.

use std::rc::Rc;
use std::thread;

fn main() {
    let rc = Rc::new(42);

    // ERRO de compilação: Rc não implementa Send
    // thread::spawn(move || {
    //     println!("{}", rc);
    // });

    // CORRETO: Arc é Send
    let arc = std::sync::Arc::new(42);
    thread::spawn(move || {
        println!("{}", arc);
    }).join().unwrap();
}

Paralelismo com rayon

Para paralelismo de dados — processar coleções em paralelo — a crate rayon é a solução idiomática. Ela implementa um pool de threads inteligente e permite converter iteradores sequenciais em paralelos com uma única mudança:

Adicione ao Cargo.toml:

[dependencies]
rayon = "1.8"
use rayon::prelude::*;

fn e_primo(n: u64) -> bool {
    if n < 2 { return false; }
    if n == 2 { return true; }
    if n % 2 == 0 { return false; }
    let limite = (n as f64).sqrt() as u64;
    (3..=limite).step_by(2).all(|i| n % i != 0)
}

fn main() {
    let numeros: Vec<u64> = (2..=100_000).collect();

    // Sequencial
    let inicio = std::time::Instant::now();
    let primos_seq: Vec<u64> = numeros.iter()
        .cloned()
        .filter(|&n| e_primo(n))
        .collect();
    let tempo_seq = inicio.elapsed();

    // Paralelo — apenas .par_iter() em vez de .iter()
    let inicio = std::time::Instant::now();
    let primos_par: Vec<u64> = numeros.par_iter()
        .cloned()
        .filter(|&n| e_primo(n))
        .collect();
    let tempo_par = inicio.elapsed();

    println!("Primos encontrados: {}", primos_seq.len());
    println!("Tempo sequencial : {:?}", tempo_seq);
    println!("Tempo paralelo   : {:?}", tempo_par);
    println!("Speedup          : {:.1}x",
        tempo_seq.as_secs_f64() / tempo_par.as_secs_f64());

    // Resultados são os mesmos (mas ordem pode diferir no paralelo)
    assert_eq!(primos_seq.len(), primos_par.len());
}

Saída típica em máquina com 8 núcleos:

Primos encontrados: 9592
Tempo sequencial : 45ms
Tempo paralelo   : 8ms
Speedup          : 5.6x

RwLock — múltiplos leitores ou um escritor

Mutex permite apenas um acesso por vez. RwLock é mais refinado: permite múltiplos leitores simultâneos, mas apenas um escritor por vez:

use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let dados = Arc::new(RwLock::new(vec![1, 2, 3, 4, 5]));
    let mut handles = Vec::new();

    // 5 threads leitoras — podem ler simultaneamente
    for id in 0..5 {
        let dados_clone = Arc::clone(&dados);
        handles.push(thread::spawn(move || {
            let leitura = dados_clone.read().unwrap();
            println!("Leitor {id}: soma = {}", leitura.iter().sum::<i32>());
        }));
    }

    for h in handles { h.join().unwrap(); }

    // 1 thread escritora — acesso exclusivo
    let dados_clone = Arc::clone(&dados);
    thread::spawn(move || {
        let mut escrita = dados_clone.write().unwrap();
        escrita.push(6);
        escrita.push(7);
        println!("Escritor adicionou elementos.");
    }).join().unwrap();

    println!("Dados finais: {:?}", dados.read().unwrap());
}

Use RwLock quando leituras são muito mais frequentes que escritas — como caches, configurações e índices.


Um programa completo: simulador de mercado

Vamos combinar tudo em uma simulação de mercado com múltiplos agentes operando em paralelo:

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

#[derive(Debug)]
struct Mercado {
    preco: f64,
    volume_total: u64,
    historico: Vec<f64>,
}

impl Mercado {
    fn novo(preco_inicial: f64) -> Self {
        Mercado {
            preco: preco_inicial,
            volume_total: 0,
            historico: vec![preco_inicial],
        }
    }

    fn comprar(&mut self, agente: &str, quantidade: u64) {
        let variacao = 1.0 + (quantidade as f64 * 0.001);
        self.preco *= variacao;
        self.volume_total += quantidade;
        self.historico.push(self.preco);
        println!("  {agente} COMPROU {quantidade} → preço: R${:.2}",
            self.preco);
    }

    fn vender(&mut self, agente: &str, quantidade: u64) {
        let variacao = 1.0 - (quantidade as f64 * 0.001);
        self.preco *= variacao;
        self.volume_total += quantidade;
        self.historico.push(self.preco);
        println!("  {agente} VENDEU  {quantidade} → preço: R${:.2}",
            self.preco);
    }
}

fn agente_comprador(mercado: Arc<Mutex<Mercado>>, nome: String, operacoes: u32) {
    for i in 0..operacoes {
        thread::sleep(Duration::from_millis(20));
        let mut m = mercado.lock().unwrap();
        m.comprar(&nome, (i as u64 + 1) * 10);
    }
}

fn agente_vendedor(mercado: Arc<Mutex<Mercado>>, nome: String, operacoes: u32) {
    for i in 0..operacoes {
        thread::sleep(Duration::from_millis(25));
        let mut m = mercado.lock().unwrap();
        m.vender(&nome, (i as u64 + 1) * 8);
    }
}

fn main() {
    let mercado = Arc::new(Mutex::new(Mercado::novo(100.0)));

    println!("── Simulação de Mercado ──");
    println!("Preço inicial: R$100.00
");

    let mut handles = Vec::new();

    // 2 compradores
    for i in 1..=2 {
        let m = Arc::clone(&mercado);
        handles.push(thread::spawn(move || {
            agente_comprador(m, format!("Comprador{i}"), 3);
        }));
    }

    // 2 vendedores
    for i in 1..=2 {
        let m = Arc::clone(&mercado);
        handles.push(thread::spawn(move || {
            agente_vendedor(m, format!("Vendedor{i}"), 3);
        }));
    }

    for h in handles { h.join().unwrap(); }

    let m = mercado.lock().unwrap();
    let preco_inicial = m.historico[0];
    let preco_final = m.preco;
    let variacao = (preco_final - preco_inicial) / preco_inicial * 100.0;

    println!("
── Relatório Final ──");
    println!("Preço inicial  : R${preco_inicial:.2}");
    println!("Preço final    : R${preco_final:.2}");
    println!("Variação       : {variacao:+.2}%");
    println!("Volume total   : {} operações", m.volume_total);
    println!("Operações      : {}", m.historico.len() - 1);

    let max = m.historico.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let min = m.historico.iter().cloned().fold(f64::INFINITY, f64::min);
    println!("Máximo         : R${max:.2}");
    println!("Mínimo         : R${min:.2}");
}

Saída (ordem das operações varia):

── Simulação de Mercado ──
Preço inicial: R$100.00

  Comprador1 COMPROU 10 → preço: R$101.00
  Vendedor1 VENDEU  8 → preço: R$100.19
  Comprador2 COMPROU 10 → preço: R$101.20
  ...

── Relatório Final ──
Preço inicial  : R$100.00
Preço final    : R$103.45
Variação       : +3.45%
Volume total   : 144 operações
Operações      : 12
Máximo         : R$108.23
Mínimo         : R$99.87

O que "concorrência sem medo" significa na prática

Rust não elimina deadlocks — esses ainda são possíveis e requerem cuidado de design. O que Rust elimina é a classe mais devastadora de bugs concorrentes: data races — acessos simultâneos a memória onde pelo menos um é uma escrita.

Em C++, você pode compilar e executar código com data race sem nenhum aviso. O resultado é comportamento indefinido — o programa pode parecer funcionar, corromper dados silenciosamente, ou crashar aleatoriamente em produção.

Em Rust, o compilador rejeita código que poderia criar data races. Se compilou, não tem data race. Essa garantia é absoluta — não é uma questão de boas práticas, é uma propriedade do sistema de tipos.

Isso é o que torna Rust a linguagem de escolha crescente para sistemas onde concorrência e confiabilidade são requisitos: kernels, servidores de alta performance, sistemas embarcados críticos.


Fontes e leituras recomendadas

  • The Rust Programming Language, Cap. 16Fearless Concurrency — https://doc.rust-lang.org/book/ch16-00-concurrency.html
  • Rust by Example — Threads — https://doc.rust-lang.org/rust-by-example/std_misc/threads.html
  • rayon — Data Parallelism in Rust — https://docs.rs/rayon
  • crossbeam crate — canais e primitivas de concorrência avançadas — https://docs.rs/crossbeam
  • "Fearless Concurrency with Rust" — Aaron Turon — artigo original do blog da Mozilla — https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html
  • "The Rustonomicon" — cap. sobre Send e Sync — https://doc.rust-lang.org/nomicon/send-and-sync.html
  • Jon Gjengset — "Crust of Rust: Atomics and Memory Ordering" — https://www.youtube.com/watch?v=rMGWeSjctlY
Comentários

Mais em Rust

Structs — Criando seus próprios tipos de dados
Structs — Criando seus próprios tipos de dados

&nbsp; At&eacute; agora trabalhamos com tipos primitivos &mdash; inteiros, s...

Testes Avançados — Propriedades, Mocks e Benchmarks
Testes Avançados — Propriedades, Mocks e Benchmarks

No Artigo #16 cobrimos os fundamentos de testes em Rust — testes unitários, d...

Rust em Produção — Observabilidade, Métricas e Operação de Sistemas Rust
Rust em Produção — Observabilidade, Métricas e Operação de Sistemas Rust

Rust — Artigo #48 Rust em Produção — Observabilidade, Métricas e Operação de...