Threads em Rust: segurança garantida pelo compilador

1. Introdução à Concorrência em Rust

Rust oferece um dos modelos de concorrência mais seguros entre linguagens de programação modernas. A base dessa segurança está no sistema de ownership e borrowing, que já conhecemos do código sequencial. Em Rust, o compilador estende essas garantias para o mundo multithread, prevenindo data races e condições de corrida em tempo de compilação.

A promessa de "fearless concurrency" (concorrência sem medo) significa que você pode escrever código concorrente confiando que o compilador detectará erros como acesso simultâneo a dados mutáveis. Diferente de linguagens como C ou C++, onde esses erros só aparecem em runtime, Rust os captura durante a compilação.

É importante distinguir concorrência (lidar com múltiplas tarefas ao mesmo tempo) de paralelismo (executar múltiplas tarefas simultaneamente). Rust suporta ambos, mas o foco deste artigo é a segurança que o compilador oferece em cenários concorrentes.

2. Criando e Gerenciando Threads com std::thread

A forma mais básica de criar threads em Rust é usando thread::spawn. Closures que capturam dados do escopo externo precisam usar a palavra-chave move para transferir ownership para a nova thread:

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(1));
        }
    });

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

    handle.join().unwrap();
}

Para capturar dados, usamos move:

let dados = vec![1, 2, 3, 4, 5];
let handle = thread::spawn(move || {
    println!("Dados recebidos: {:?}", dados);
});
handle.join().unwrap();
// println!("{:?}", dados); // Erro! Ownership foi movido

join() aguarda a thread terminar e retorna um Result que pode conter erros de pânico.

3. Compartilhamento de Dados com Arc<T> e Mutex<T>

Quando precisamos compartilhar dados entre múltiplas threads, usamos Arc<T> (Atomic Reference Counting) para ownership compartilhado e Mutex<T> para acesso mutável seguro:

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

fn main() {
    let contador = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let contador = Arc::clone(&contador);
        handles.push(thread::spawn(move || {
            let mut num = contador.lock().unwrap();
            *num += 1;
        }));
    }

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

    println!("Resultado: {}", *contador.lock().unwrap());
}

Arc permite que múltiplas threads possuam referências ao mesmo dado, enquanto Mutex garante que apenas uma thread por vez possa modificar o valor. A combinação Arc<Mutex<T>> é o padrão mais comum para dados mutáveis compartilhados.

4. Comunicação entre Threads com Canais (mpsc)

O módulo std::sync::mpsc implementa canais para comunicação entre threads no padrão produtor-consumidor:

use std::sync::mpsc;
use std::thread;

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

    thread::spawn(move || {
        let mensagens = vec!["olá", "mundo", "rust"];
        for msg in mensagens {
            tx.send(msg).unwrap();
            thread::sleep(std::time::Duration::from_secs(1));
        }
    });

    for recebida in rx {
        println!("Recebido: {}", recebida);
    }
}

Para múltiplos produtores, clonamos o Sender:

let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();

thread::spawn(move || {
    tx.send("da thread 1").unwrap();
});

thread::spawn(move || {
    tx1.send("da thread 2").unwrap();
});

for msg in rx {
    println!("{}", msg);
}

5. Garantias de Segurança do Compilador

O compilador Rust previne data races através do borrow checker. Os traits Send e Sync são fundamentais:

  • Send: tipos que podem ter ownership transferido entre threads com segurança
  • Sync: tipos que podem ser referenciados por múltiplas threads simultaneamente

A maioria dos tipos implementa esses traits automaticamente, mas tipos como Rc<T> não são Send:

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

fn main() {
    let dados = Rc::new(5);
    thread::spawn(move || {
        println!("{}", dados); // Erro! Rc não implementa Send
    });
}

Este código não compila porque Rc não é seguro para threads. O compilador nos força a usar Arc no lugar.

6. Padrões Avançados com RwLock<T> e Condvar

RwLock permite múltiplos leitores ou um único escritor:

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

fn main() {
    let cache = Arc::new(RwLock::new(String::from("dados iniciais")));
    let mut handles = vec![];

    // Leitores concorrentes
    for _ in 0..5 {
        let cache = Arc::clone(&cache);
        handles.push(thread::spawn(move || {
            let leitura = cache.read().unwrap();
            println!("Lendo: {}", *leitura);
        }));
    }

    // Escritor exclusivo
    let cache = Arc::clone(&cache);
    handles.push(thread::spawn(move || {
        let mut escrita = cache.write().unwrap();
        *escrita = String::from("dados atualizados");
    }));

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

7. Threads com Escopo (Scoped Threads)

Desde Rust 1.63, thread::scope permite criar threads que podem emprestar dados sem precisar de Arc:

use std::thread;

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

    thread::scope(|s| {
        s.spawn(|| {
            println!("Thread 1: {:?}", &dados);
        });
        s.spawn(|| {
            println!("Thread 2: {:?}", &dados);
        });
    });

    println!("Dados ainda disponíveis: {:?}", dados);
}

As threads com escopo garantem que todas terminam antes do escopo fechar, permitindo referências seguras sem overhead de contagem de referências.

8. Boas Práticas e Armadilhas Comuns

Evitando deadlocks: mantenha uma ordem consistente ao adquirir múltiplos locks e use try_lock quando apropriado:

use std::sync::{Mutex, MutexGuard};

fn transferir(origem: &Mutex<i32>, destino: &Mutex<i32>, valor: i32) {
    let (primeiro, segundo) = if std::ptr::eq(origem, destino) {
        (origem, destino)
    } else if std::ptr::addr_of!(*origem) < std::ptr::addr_of!(*destino) {
        (origem, destino)
    } else {
        (destino, origem)
    };

    let mut a = primeiro.lock().unwrap();
    let mut b = segundo.lock().unwrap();
    *a -= valor;
    *b += valor;
}

Performance: criar threads tem custo. Para tarefas curtas, use thread pools com crates como rayon:

use rayon::prelude::*;

fn main() {
    let numeros: Vec<i32> = (0..1000).collect();
    let soma: i32 = numeros.par_iter().sum();
    println!("Soma paralela: {}", soma);
}

Testando concorrência: ferramentas como loom ajudam a detectar bugs em código concorrente durante testes.

Referências