Send e Sync: os traits de concorrência

1. Introdução aos traits de concorrência

Programação concorrente é notoriamente difícil. Data races, deadlocks e comportamento indefinido são apenas alguns dos problemas que assombram desenvolvedores em linguagens como C e C++. Rust oferece uma abordagem radicalmente diferente: garantir segurança em tempo de compilação através de seu sistema de tipos.

Dois traits fundamentais tornam isso possível: Send e Sync. Eles são traits marker — não possuem métodos — mas carregam um significado profundo para o compilador. Send indica que um valor pode ser transferido com segurança para outra thread, enquanto Sync indica que múltiplas threads podem acessar uma referência compartilhada ao mesmo tempo sem causar data races.

2. O trait Send: transferência de ownership entre threads

O trait Send é implementado para tipos cujo ownership pode ser transferido entre threads sem risco de corrupção de dados. Tipos primitivos como i32, f64 e bool implementam Send. Coleções como String e Vec<T> também são Send desde que seus elementos também sejam.

fn transferir_para_thread() {
    let dados = String::from("Olá do Rust");

    std::thread::spawn(move || {
        // `dados` foi movido para esta thread com segurança
        println!("{}", dados);
    }).join().unwrap();
}

Tipos que não são Send incluem Rc<T> (contador de referência não atômico) e ponteiros raw como *const T. Rc<T> não é Send porque o contador de referência não é atômico, o que poderia causar corrupção se duas threads tentassem modificar o contador simultaneamente.

use std::rc::Rc;

fn exemplo_rc_nao_send() {
    let rc = Rc::new(5);

    // Isto não compila: `Rc<i32>` não implementa `Send`
    // std::thread::spawn(move || {
    //     println!("{}", rc);
    // });
}

A implementação de Send é automática para a maioria dos tipos. Apenas quando você cria um tipo que contém ponteiros raw ou tipos não Send, você precisa implementar manualmente usando unsafe.

3. O trait Sync: acesso compartilhado entre threads

Sync é implementado para tipos que podem ser referenciados (&T) por múltiplas threads simultaneamente. Formalmente, T: Sync significa que &T: Send. Se você pode enviar uma referência para outra thread, o tipo subjacente é Sync.

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

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

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

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

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

Mutex<T> é Sync quando T: Send, pois o mutex garante acesso exclusivo. AtomicU32 e outros tipos atômicos são Sync por natureza. Já RefCell<T> não é Sync porque suas verificações de borrow ocorrem em tempo de execução sem sincronização entre threads.

4. A interação entre Send e Sync

A relação entre Send e Sync pode ser visualizada como uma hierarquia. Um tipo pode ser:

Combinação Significado Exemplos
Send + Sync Totalmente thread-safe i32, Mutex<T>, Arc<T>
Send - Sync Pode ser movido, mas não compartilhado RefCell<T> (em single-thread)
- Send + Sync Raro: referência segura, mas ownership não *const T (com garantias)
- Send - Sync Não thread-safe Rc<T>, *mut T

Um caso interessante é Mutex<T>: se T: Send, então Mutex<T>: Sync. Isso faz sentido porque o mutex protege o acesso interno, permitindo que threads compartilhem o mutex mesmo que o valor interno precise ser movido entre threads.

// `Mutex<i32>` implementa tanto Send quanto Sync
fn verificar_traits() {
    fn is_send<T: Send>() {}
    fn is_sync<T: Sync>() {}

    is_send::<Mutex<i32>>();
    is_sync::<Mutex<i32>>();
}

5. Implementação unsafe de Send e Sync

Em situações especiais, como interfaces FFI ou tipos que encapsulam ponteiros raw, você pode precisar implementar Send ou Sync manualmente. Isso requer o uso de unsafe impl e assume a responsabilidade de garantir a segurança manualmente.

struct WrapperFFI {
    ptr: *mut i32,
}

// Assumimos que o ponteiro aponta para memória alocada que é thread-safe
unsafe impl Send for WrapperFFI {}
unsafe impl Sync for WrapperFFI {}

impl WrapperFFI {
    fn new(valor: i32) -> Self {
        let ptr = Box::into_raw(Box::new(valor));
        WrapperFFI { ptr }
    }

    fn obter(&self) -> i32 {
        unsafe { *self.ptr }
    }
}

fn usar_wrapper_ffi() {
    let wrapper = WrapperFFI::new(42);
    let arc = std::sync::Arc::new(wrapper);
    let arc_clone = Arc::clone(&arc);

    std::thread::spawn(move || {
        println!("Valor na thread: {}", arc_clone.obter());
    }).join().unwrap();
}

Implementar Send ou Sync incorretamente pode levar a data races e undefined behavior. O compilador confia no programador — uma responsabilidade que não deve ser tomada levianamente.

6. Padrões comuns e boas práticas

O padrão Arc<Mutex<T>> é onipresente em Rust para dados compartilhados entre threads. Arc fornece contagem de referência atômica (tornando-o Send e Sync), enquanto Mutex protege o acesso ao valor interno.

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

fn padrao_compartilhado() {
    let dados = Arc::new(Mutex::new(vec![1, 2, 3]));
    let mut handles = vec![];

    for i in 0..5 {
        let dados_clone = Arc::clone(&dados);
        handles.push(std::thread::spawn(move || {
            let mut dados = dados_clone.lock().unwrap();
            dados.push(i);
        }));
    }

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

    println!("Dados: {:?}", *dados.lock().unwrap());
}

Use Rc apenas em contextos single-thread. Quando precisar de mutabilidade interior em múltiplas threads, prefira Mutex ou RwLock em vez de RefCell. O compilador guiará você com mensagens de erro claras quando você tentar usar um tipo não Send em um contexto que exige concorrência.

7. Exemplos avançados e armadilhas

Criar um tipo que não é Send nem Sync pode ser útil para garantir que certos recursos não sejam usados indevidamente entre threads:

use std::marker::PhantomData;

struct RecursoThreadUnsafe {
    // Impede que o tipo seja Send ou Sync
    _nao_send: PhantomData<*const ()>,
    _nao_sync: PhantomData<*mut ()>,
}

impl RecursoThreadUnsafe {
    fn new() -> Self {
        RecursoThreadUnsafe {
            _nao_send: PhantomData,
            _nao_sync: PhantomData,
        }
    }
}

fn exemplo_nao_thread_safe() {
    let recurso = RecursoThreadUnsafe::new();

    // Isto não compila:
    // std::thread::spawn(move || {
    //     let _ = recurso;
    // });
}

PhantomData é uma ferramenta poderosa para controlar como o compilador trata traits marker em estruturas genéricas. Ele permite que você simule ownership de tipos que não estão fisicamente presentes na estrutura.

A função std::thread::spawn exige que a closure seja Send, pois ela é movida para a nova thread. Isso é uma das principais maneiras pelas quais o sistema de tipos de Rust previne data races em tempo de compilação.

fn spawn_exige_send() {
    let valor = String::from("seguro");

    // Funciona: String implementa Send
    let handle = std::thread::spawn(move || {
        println!("{}", valor);
    });

    handle.join().unwrap();
}

Em interfaces FFI, é comum encontrar *mut T que precisam de implementações unsafe de Send e Sync. Sempre verifique a documentação da biblioteca C/C++ para garantir que o acesso concorrente é seguro antes de implementar esses traits.

O sistema de Send e Sync é uma das maiores inovações de Rust na prevenção de erros de concorrência. Em vez de confiar em verificações em tempo de execução ou na disciplina do programador, Rust torna as garantias de segurança parte do sistema de tipos, verificadas em tempo de compilação sem custo de performance.

Referências