Send e Sync
Esta aula explica as traits Send e Sync do Rust, que controlam a transferência e o compartilhamento de dados entre threads. São mecanismos de segurança em tempo de compilação que previnem corridas de dados e outras condições de concorrência.
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 éSendnemSyncporque usa contagem de referência não atômica, o que causaria corridas de dados se compartilhado entre threads.Cell<T>eRefCell<T>: não sãoSyncporque permitem mutação interior sem bloqueio, o que não é seguro para acesso concorrente.UnsafeCell<T>: não éSyncpor definição; é a base para tipos com mutação interior.- Ponteiros brutos (
*const T,*mut T): não sãoSendnemSyncpor 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 é
Sendse e somente se todos os seus campos sãoSend(a menos que implementado manualmente). - Um tipo é
Syncse e somente se todos os seus campos sãoSync(a menos que implementado manualmente). - Tipos que contêm
UnsafeCellnão são automaticamenteSync.
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
SendeSynccorretamente. - Ao criar tipos próprios para concorrência, implemente
SendeSyncmanualmente apenas se você entender completamente as implicações de segurança. - Use
MutexouRwLockpara compartilhar dados mutáveis entre threads, eArcpara compartilhar propriedade. - Evite usar
unsafe impl Sendouunsafe impl Synca menos que seja absolutamente necessário e você tenha verificado a segurança manualmente.
Referências
- Documentação oficial da trait Send
- Documentação oficial da trait Sync
- Rustonomicon: Send and Sync
- The Rust Book: Concurrency with Sync and Send
- Documentação do módulo std::thread
- Documentação do módulo std::sync
Exercícios
Explique por que
Rc<T>não implementaSendnemSync.✓ Resposta:Rcusa 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 éSyncporque permitiria acesso imutável compartilhado, mas a contagem interna ainda seria mutada sem sincronização.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, porqueCell<i32>não implementaSync. Tentar compartilhar uma referência aCellentre threads resultaria em erro de compilação. O motivo é queCellpermite mutação interior sem qualquer bloqueio, o que causaria corridas de dados se acessado concorrentemente.Escreva um exemplo de código que compile e use
ArceMutexpara 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] }Explique a relação entre
SynceSend: por queT: Syncimplica que&TéSend?✓ Resposta: A definição deSyncé 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.Dado um tipo
struct MinhaEstrutura { x: i32, y: String }, ele implementa automaticamenteSendeSync? Justifique.✓ Resposta: Sim, implementa automaticamenteSendeSyncporque todos os seus campos (i32eString) implementamSendeSync. O compilador deriva essas traits automaticamente para structs cujos campos são todosSend/Sync, a menos que explicitamente opte-se por não derivar ou useUnsafeCell.