Em Rust, o sistema de ownership e borrowing garante segurança de memória em tempo de compilação, mas quando trabalhamos com concorrência, precisamos de mecanismos adicionais para compartilhar dados entre threads de forma segura. Arc (Atomic Reference Counting) e Mutex (Mutual Exclusion) são duas ferramentas fundamentais para esse fim. Nesta aula, vamos explorar como usá-los corretamente, entender suas diferenças com Rc, lidar com poisoning e evitar deadlocks.

Compartilhamento entre threads

Para compartilhar dados entre threads, precisamos que o tipo implemente a trait Send (pode ser transferido entre threads) e Sync (pode ser referenciado por múltiplas threads simultaneamente). Arc fornece uma referência contada atômica que permite que múltiplas threads possuam acesso compartilhado ao mesmo dado, desde que o tipo interno seja Sync. Mutex, por sua vez, garante que apenas uma thread por vez acesse o dado, protegendo contra condições de corrida.

Um padrão comum é combinar Arc e Mutex: Arc<Mutex<T>>. O Arc permite que várias threads mantenham uma referência ao Mutex, e o Mutex serializa o acesso ao valor interno.

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!("Result: {}", *counter.lock().unwrap());
}

Neste exemplo, 10 threads incrementam um contador compartilhado. O Arc::clone incrementa a contagem de referências, e lock() retorna um guard que desbloqueia automaticamente ao sair de escopo.

Arc vs Rc

Rc (Reference Counting) é usado para compartilhamento dentro de uma única thread, pois não é Send nem Sync. Arc é a versão atômica de Rc, adequada para ambientes concorrentes. A diferença principal é que Arc usa operações atômicas para gerenciar a contagem de referências, o que tem um custo de desempenho maior, mas é necessário para segurança entre threads. Rc é mais leve e deve ser preferido quando não há concorrência.

Exemplo: se você tentar usar Rc em múltiplas threads, o compilador rejeitará:

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

fn main() {
    let data = Rc::new(42);
    thread::spawn(move || {
        println!("{}", data); // erro: Rc<i32> não implementa Send
    });
}

Para corrigir, troque Rc por Arc:

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(42);
    let data_clone = Arc::clone(&data);
    thread::spawn(move || {
        println!("{}", data_clone);
    });
}

Mutex e poisoning

Um Mutex em Rust pode estar em estado "poisoned" (envenenado) quando uma thread entra em pânico enquanto detém o lock. Quando isso acontece, o Mutex marca seu estado como poisoned para evitar que outras threads acessem dados potencialmente corrompidos. Tentar chamar lock() em um Mutex poisoned retorna um Err (especificamente PoisonError).

Para lidar com isso, podemos usar lock().unwrap_or_else() ou ignorar o erro com lock().unwrap() (que também causaria pânico). A melhor prática é tratar o erro adequadamente, por exemplo, resetando o valor ou propagando o erro.

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

fn main() {
    let mutex = Mutex::new(0);

    // Simulando um pânico enquanto segura o lock
    let handle = std::thread::spawn(move || {
        let _guard = mutex.lock().unwrap();
        panic!("Oops!");
    });
    // mutex foi movido para a thread, então não podemos mais usar aqui
    // Para demonstrar, criaremos um novo mutex

    let mutex2 = Mutex::new(10);
    let result = std::panic::catch_unwind(|| {
        let _guard = mutex2.lock().unwrap();
        panic!("Panic inside");
    });
    // mutex2 está poisoned
    match mutex2.lock() {
        Ok(mut val) => *val += 1,
        Err(e) => {
            println!("Mutex poisoned, recovering...");
            let mut val = e.into_inner();
            *val = 0; // reset
        }
    }
    println!("Final value: {}", *mutex2.lock().unwrap());
}

No código acima, capturamos o pânico e tratamos o erro de poisoning, recuperando o lock interno com into_inner().

Deadlocks

Deadlock ocorre quando duas ou mais threads ficam bloqueadas esperando por locks que nunca serão liberados, geralmente devido a uma ordem inconsistente de aquisição. Em Rust, deadlocks não são evitados pelo compilador; cabe ao programador projetar o código para evitá-los.

Uma estratégia comum é sempre adquirir os locks na mesma ordem em todas as threads. Outra é usar try_lock() que não bloqueia, permitindo que a thread desista se não conseguir o lock imediatamente.

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

fn main() {
    let resource_a = Arc::new(Mutex::new(0));
    let resource_b = Arc::new(Mutex::new(0));

    let a1 = Arc::clone(&resource_a);
    let b1 = Arc::clone(&resource_b);
    let handle1 = thread::spawn(move || {
        let _lock_a = a1.lock().unwrap();
        thread::sleep(std::time::Duration::from_millis(10));
        let _lock_b = b1.lock().unwrap(); // pode causar deadlock se outra thread fizer o oposto
    });

    let a2 = Arc::clone(&resource_a);
    let b2 = Arc::clone(&resource_b);
    let handle2 = thread::spawn(move || {
        let _lock_b = b2.lock().unwrap();
        thread::sleep(std::time::Duration::from_millis(10));
        let _lock_a = a2.lock().unwrap(); // ordem inversa -> deadlock
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

Para evitar o deadlock, garantir que ambas as threads adquiram os locks na mesma ordem (por exemplo, sempre A antes de B). Alternativamente, usar try_lock com backoff.

Boas práticas

  • Prefira Arc<Mutex<T>> apenas quando necessário; para dados imutáveis, use Arc<T> diretamente.
  • Mantenha o escopo do lock o menor possível para evitar contenção e deadlocks.
  • Evite chamar funções desconhecidas ou que possam causar pânico enquanto segura um lock.
  • Considere usar RwLock para cenários de leitura frequente e escrita esporádica.
  • Use ferramentas como parking_lot::Mutex para melhor desempenho em alguns casos, mas entenda as diferenças.

Referências

Exercícios

  1. Escreva um programa que cria 5 threads, cada uma incrementando um contador compartilhado (usando Arc e Mutex) 1000 vezes. Ao final, imprima o valor do contador.

    ✓ Resposta:
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    fn main() {
        let counter = Arc::new(Mutex::new(0));
        let mut handles = vec![];
    
        for _ in 0..5 {
            let counter = Arc::clone(&counter);
            let handle = thread::spawn(move || {
                for _ in 0..1000 {
                    let mut num = counter.lock().unwrap();
                    *num += 1;
                }
            });
            handles.push(handle);
        }
    
        for handle in handles {
            handle.join().unwrap();
        }
    
        println!("Counter: {}", *counter.lock().unwrap());
    }
  2. Explique por que o código abaixo não compila e como corrigi-lo:

    use std::rc::Rc;
    use std::thread;
    
    fn main() {
        let data = Rc::new(42);
        let handle = thread::spawn(move || {
            println!("{}", data);
        });
        handle.join().unwrap();
    }

    ✓ Resposta: O erro é que Rc<i32> não implementa Send, portanto não pode ser transferido para outra thread. A correção é usar Arc no lugar de Rc. Além disso, é necessário clonar o Arc para cada thread. Código corrigido:
    use std::sync::Arc;
    use std::thread;
    
    fn main() {
        let data = Arc::new(42);
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            println!("{}", data_clone);
        });
        handle.join().unwrap();
    }
  3. Crie um cenário onde um Mutex fica poisoned e implemente uma recuperação que resete o valor para 0.

    ✓ Resposta:
    use std::sync::{Mutex, PoisonError};
    
    fn main() {
        let mutex = Mutex::new(10);
        let result = std::panic::catch_unwind(|| {
            let _guard = mutex.lock().unwrap();
            panic!("Panic!");
        });
        // mutex está poisoned
        match mutex.lock() {
            Ok(mut val) => println!("Value: {}", *val),
            Err(e) => {
                let mut val = e.into_inner();
                *val = 0;
                println!("Reset to 0");
            }
        }
        println!("Final: {}", *mutex.lock().unwrap());
    }
  4. Escreva um código que cause um deadlock entre duas threads usando dois Mutexes e depois corrija-o garantindo ordem consistente.

    ✓ Resposta: Código com deadlock:
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    fn main() {
        let a = Arc::new(Mutex::new(0));
        let b = Arc::new(Mutex::new(0));
        let a1 = Arc::clone(&a);
        let b1 = Arc::clone(&b);
        let h1 = thread::spawn(move || {
            let _la = a1.lock().unwrap();
            thread::sleep(std::time::Duration::from_millis(10));
            let _lb = b1.lock().unwrap();
        });
        let a2 = Arc::clone(&a);
        let b2 = Arc::clone(&b);
        let h2 = thread::spawn(move || {
            let _lb = b2.lock().unwrap();
            thread::sleep(std::time::Duration::from_millis(10));
            let _la = a2.lock().unwrap();
        });
        h1.join().unwrap();
        h2.join().unwrap();
    }
    Correção: ambas as threads adquirem os locks na mesma ordem (por exemplo, a antes de b):
    // ... mesma configuração, mas h2:
    let h2 = thread::spawn(move || {
        let _la = a2.lock().unwrap();
        thread::sleep(std::time::Duration::from_millis(10));
        let _lb = b2.lock().unwrap();
    });
  5. Usando try_lock, implemente um mecanismo que evite deadlock: cada thread tenta adquirir ambos os locks, e se falhar, libera o lock já obtido e tenta novamente após um curto período.

    ✓ Resposta:
    use std::sync::{Arc, Mutex};
    use std::thread;
    use std::time::Duration;
    
    fn main() {
        let a = Arc::new(Mutex::new(0));
        let b = Arc::new(Mutex::new(0));
        let a1 = Arc::clone(&a);
        let b1 = Arc::clone(&b);
        let h1 = thread::spawn(move || loop {
            if let Ok(la) = a1.try_lock() {
                if let Ok(lb) = b1.try_lock() {
                    // critical section
                    break;
                } else {
                    drop(la);
                    thread::sleep(Duration::from_millis(1));
                }
            } else {
                thread::sleep(Duration::from_millis(1));
            }
        });
        let a2 = Arc::clone(&a);
        let b2 = Arc::clone(&b);
        let h2 = thread::spawn(move || loop {
            if let Ok(lb) = b2.try_lock() {
                if let Ok(la) = a2.try_lock() {
                    // critical section
                    break;
                } else {
                    drop(lb);
                    thread::sleep(Duration::from_millis(1));
                }
            } else {
                thread::sleep(Duration::from_millis(1));
            }
        });
        h1.join().unwrap();
        h2.join().unwrap();
    }