Em Rust, a segurança de concorrência é garantida pelo sistema de tipos através de duas traits fundamentais: Send e Sync. Elas são automaticamente implementadas para a maioria dos tipos, mas podem ser manualmente marcadas ou removidas quando necessário. Compreender essas traits é essencial para escrever código concorrente correto e eficiente.

Essas traits são marcadores (marker traits) — não possuem métodos — e servem como contratos para o compilador. Send indica que um valor pode ser transferido com segurança para outra thread, enquanto Sync indica que uma referência a um valor pode ser compartilhada entre threads. Juntas, elas formam a base para APIs como std::thread::spawn e Arc.

O que garantem

A trait Send garante que a propriedade de um valor pode ser transferida entre threads sem causar comportamento indefinido. Isso significa que o tipo não compartilha estado interno mutável de forma insegura (por exemplo, sem sincronização). A maioria dos tipos em Rust são Send, incluindo tipos primitivos, String, Vec, e tipos compostos que contêm apenas tipos Send.

A trait Sync garante que um valor pode ser referenciado (imutavelmente) por múltiplas threads simultaneamente. Mais precisamente, T: Sync significa que &T é Send. Tipos que são Sync incluem tipos primitivos, Mutex<T> (quando T: Send), Arc<T> (quando T: Send + Sync), e tipos atômicos. Tipos como Cell<T> e RefCell<T> não são Sync porque permitem mutação interior sem sincronização.

use std::thread;

fn main() {
    let x = 42;
    // i32 implementa Send e Sync
    thread::spawn(move || {
        println!("Valor: {}", x);
    }).join().unwrap();
}

Por que importam

Sem essas traits, o compilador não poderia detectar corridas de dados em tempo de compilação. Por exemplo, se um tipo não é Send, tentar movê-lo para uma nova thread resulta em erro de compilação. Isso evita que o programador acidentalmente exponha dados mutáveis a múltiplas threads sem proteção.

Além disso, as traits permitem que bibliotecas concorrentes (como std::thread, Arc, Mutex) exijam garantias de segurança através de bounds genéricos. Por exemplo, thread::spawn exige que a closure seja Send (pois ela é enviada para a nova thread) e que o tipo retornado seja Send (para ser enviado de volta). Da mesma forma, Arc só pode ser usado se o tipo interno for Send + Sync.

use std::rc::Rc;

// Rc não é Send, então isso não compila:
// thread::spawn(|| {
//     let rc = Rc::new(5);
//     println!("{}", rc);
// });

// Mas Arc é Send:
use std::sync::Arc;
let arc = Arc::new(5);
thread::spawn(move || {
    println!("{}", arc);
});

Tipos que não são

Alguns tipos intencionalmente não implementam Send ou Sync para evitar uso inseguro. Os exemplos mais comuns são:

  • Rc<T>: não é Send nem Sync porque usa contagem de referência não atômica, o que causaria corridas de dados se compartilhado entre threads.
  • Cell<T> e RefCell<T>: não são Sync porque permitem mutação interior sem bloqueio, o que não é seguro para acesso concorrente.
  • UnsafeCell<T>: não é Sync por definição; é a base para tipos com mutação interior.
  • Ponteiros brutos (*const T, *mut T): não são Send nem Sync por padrão, pois não há garantias de segurança.

É possível implementar manualmente Send ou Sync para tipos próprios usando unsafe impl, mas isso deve ser feito com cuidado, pois o compilador confia na correção do programador.

use std::cell::Cell;

// Cell não é Sync, então isso não compila:
// let cell = Cell::new(42);
// thread::spawn(|| {
//     println!("{}", cell.get());
// });

// Mas podemos usar Mutex para torná-lo seguro:
use std::sync::Mutex;
let mutex = Mutex::new(42);
thread::spawn(move || {
    let val = mutex.lock().unwrap();
    println!("{}", *val);
});

Segurança de concorrência

As traits Send e Sync são a espinha dorsal da segurança de concorrência em Rust. Elas são verificadas estaticamente, sem custo em tempo de execução. Isso significa que muitos erros comuns em outras linguagens (como corridas de dados) são capturados na compilação.

Para garantir segurança, o compilador aplica as seguintes regras:

  • Um tipo é Send se e somente se todos os seus campos são Send (a menos que implementado manualmente).
  • Um tipo é Sync se e somente se todos os seus campos são Sync (a menos que implementado manualmente).
  • Tipos que contêm UnsafeCell não são automaticamente Sync.

Além disso, a biblioteca padrão fornece primitivas de sincronização como Mutex, RwLock, Arc, e tipos atômicos que implementam Send e Sync quando apropriado. Por exemplo, Mutex<T> é Sync apenas se T: Send, pois o mutex permite compartilhar o acesso a T entre threads.

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

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));

    let mut handles = vec![];
    for i in 0..3 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let mut vec = data.lock().unwrap();
            vec.push(i);
        }));
    }

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

    println!("{:?}", data.lock().unwrap());
}

Boas práticas

  • Prefira usar tipos da biblioteca padrão que já implementam Send e Sync corretamente.
  • Ao criar tipos próprios para concorrência, implemente Send e Sync manualmente apenas se você entender completamente as implicações de segurança.
  • Use Mutex ou RwLock para compartilhar dados mutáveis entre threads, e Arc para compartilhar propriedade.
  • Evite usar unsafe impl Send ou unsafe impl Sync a menos que seja absolutamente necessário e você tenha verificado a segurança manualmente.

Referências

Exercícios

  1. Explique por que Rc<T> não implementa Send nem Sync.

    ✓ Resposta: Rc usa contagem de referência não atômica (usando operações de contagem que não são thread-safe). Se fosse enviado para outra thread, a contagem poderia ser incrementada/decrementada simultaneamente, causando corrida de dados e potencial corrupção de memória. Portanto, não é Send. Também não é Sync porque permitiria acesso imutável compartilhado, mas a contagem interna ainda seria mutada sem sincronização.
  2. O que aconteceria se você tentasse usar Cell<i32> em múltiplas threads sem sincronização? O compilador permitiria? Por quê?

    ✓ Resposta: O compilador não permitiria, porque Cell<i32> não implementa Sync. Tentar compartilhar uma referência a Cell entre threads resultaria em erro de compilação. O motivo é que Cell permite mutação interior sem qualquer bloqueio, o que causaria corridas de dados se acessado concorrentemente.
  3. Escreva um exemplo de código que compile e use Arc e Mutex para compartilhar um vetor mutável entre duas threads.

    ✓ Resposta:
    use std::sync::{Arc, Mutex};
    use std::thread;
    
    fn main() {
        let data = Arc::new(Mutex::new(vec![1, 2, 3]));
        let data2 = Arc::clone(&data);
    
        let handle1 = thread::spawn(move || {
            let mut vec = data.lock().unwrap();
            vec.push(4);
        });
    
        let handle2 = thread::spawn(move || {
            let mut vec = data2.lock().unwrap();
            vec.push(5);
        });
    
        handle1.join().unwrap();
        handle2.join().unwrap();
    
        println!("{:?}", data.lock().unwrap()); // [1,2,3,4,5]
    }
  4. Explique a relação entre Sync e Send: por que T: Sync implica que &T é Send?

    ✓ Resposta: A definição de Sync é exatamente que &T é Send. Se um tipo é Sync, significa que é seguro para múltiplas threads acessarem uma referência imutável simultaneamente. Consequentemente, essa referência pode ser enviada para outra thread, pois não há mutação concorrente. Portanto, &T é Send.
  5. Dado um tipo struct MinhaEstrutura { x: i32, y: String }, ele implementa automaticamente Send e Sync? Justifique.

    ✓ Resposta: Sim, implementa automaticamente Send e Sync porque todos os seus campos (i32 e String) implementam Send e Sync. O compilador deriva essas traits automaticamente para structs cujos campos são todos Send/Sync, a menos que explicitamente opte-se por não derivar ou use UnsafeCell.