Send e Sync
Esta aula explora as traits Send e Sync do Rust, fundamentais para garantir segurança de concorrência. Você aprenderá o que essas traits garantem, por que são importantes, quais tipos não as implementam e como elas se relacionam com a segurança de concorrência.
Em Rust, a segurança de concorrência é garantida em tempo de compilação através de um sistema de tipos expressivo. Duas traits fundamentais nesse contexto são Send e Sync. Elas são automaticamente implementadas pelo compilador para a maioria dos tipos, mas entender seu funcionamento é crucial para escrever código concorrente seguro.
Esta aula explora o que essas traits garantem, por que são importantes, quais tipos não as implementam e como elas se relacionam com a segurança de concorrência.
O que garantem
A trait Send indica que a propriedade de um valor pode ser transferida com segurança entre threads. Em outras palavras, se um tipo implementa Send, você pode mover um valor desse tipo para outra thread sem causar problemas de segurança de memória. A maioria dos tipos em Rust implementa Send, incluindo tipos primitivos, Vec, String, etc.
A trait Sync indica que um valor pode ser compartilhado com segurança entre threads através de uma referência. Ou seja, se um tipo implementa Sync, você pode criar uma referência imutável (&T) para um valor e essa referência pode ser acessada por múltiplas threads simultaneamente sem causar corridas de dados. Geralmente, tipos que implementam Sync também são Send, mas o contrário não é verdade.
Exemplo de código:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
// Mover v para a thread: precisa ser Send
thread::spawn(move || {
println!("{:?}", v);
}).join().unwrap();
}
Neste exemplo, Vec<i32> implementa Send, então podemos mover v para a nova thread.
Por que importam
Send e Sync são a base do sistema de segurança de concorrência do Rust. Elas permitem que o compilador verifique estaticamente que seu código não viola regras de concorrência, prevenindo corridas de dados e outros problemas. Sem essas traits, seria fácil introduzir bugs sutis que são difíceis de depurar.
Além disso, essas traits são marcadores (marker traits) sem métodos. Sua implementação é automática para a maioria dos tipos, mas você pode implementá-las manualmente (usando unsafe) para tipos personalizados quando tiver certeza de que são seguros. Isso é útil ao criar abstrações concorrentes seguras, como Mutex, Arc e canais.
Exemplo de código:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(5);
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("{}", data);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
Aqui, Arc<T> implementa Send e Sync quando T: Send + Sync, permitindo compartilhar o valor entre threads com segurança.
Tipos que não são
Nem todos os tipos implementam Send ou Sync. Por exemplo, Rc<T> não implementa Send porque seu contador de referências não é atômico e pode ser corrompido se movido entre threads. Da mesma forma, RefCell<T> não implementa Sync porque suas regras de empréstimo são verificadas em tempo de execução e não são seguras para acesso simultâneo.
Outros exemplos: tipos que contêm ponteiros brutos ou mutexes não implementam automaticamente essas traits. Você precisa verificar a documentação ou usar ferramentas como cargo doc --document-private-items para ver a implementação de traits.
Exemplo de código:
use std::rc::Rc;
fn main() {
let rc = Rc::new(5);
// Isto não compila: Rc não implementa Send
// thread::spawn(move || {
// println!("{}", rc);
// });
}
Se você tentar mover um Rc para uma thread, o compilador emitirá um erro informando que a trait Send não é implementada.
Segurança de concorrência
Send e Sync são os pilares da segurança de concorrência em Rust. Elas garantem que você não possa acidentalmente introduzir corridas de dados ou comportamento indefinido ao compartilhar dados entre threads. O compilador verifica que todos os tipos usados em operações concorrentes implementam as traits necessárias.
Por exemplo, ao usar thread::spawn, o fechamento passado deve ser Send, pois será movido para a nova thread. Ao compartilhar uma referência imutável entre threads, o tipo referenciado deve ser Sync. Essas verificações são feitas em tempo de compilação, eliminando uma classe inteira de bugs.
Além disso, a biblioteca padrão fornece tipos como Mutex, RwLock e Atomic que implementam Send e Sync quando apropriado, facilitando a escrita de código concorrente seguro.
Exemplo de código:
use std::sync::{Mutex, Arc};
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);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Aqui, Mutex<T> implementa Send e Sync quando T: Send, permitindo acesso seguro a dados compartilhados.
Referências
- Documentação oficial de Send
- Documentação oficial de Sync
- O Livro do Rust: Concorrência extensível com Sync e Send
- Rustonomicon: Send e Sync
- Too Many Lists: Send e Sync
Exercícios
Explique por que
Rc<T>não implementaSend.✓ Resposta:Rc<T>não implementaSendporque seu contador de referências não é atômico. Se umRcfosse movido para outra thread, os incrementos/decrementos do contador poderiam causar corridas de dados, levando a vazamentos de memória ou dupla liberação.O que aconteceria se você tentasse usar
thread::spawncom um fechamento que captura umRc? Mostre um exemplo e a mensagem de erro esperada.✓ Resposta: O compilador emitirá um erro informando que a traitSendnão é implementada paraRc<i32>. Exemplo:
Erro:use std::rc::Rc; use std::thread; fn main() { let rc = Rc::new(5); thread::spawn(move || { println!("{}", rc); }).join().unwrap(); }error[E0277]: `Rc<i32>` cannot be sent between threads safelyDê um exemplo de um tipo que implementa
Sendmas nãoSync.✓ Resposta: Um exemplo éCell<T>(excetoCellcom tipos não-Copy).Cell<T>implementaSendquandoT:Send, mas não implementaSyncporque permite mutação interior através de referências compartilhadas, o que pode causar corridas de dados.Como você pode implementar manualmente
SendeSyncpara um tipo personalizado? Isso é seguro?✓ Resposta: Você pode implementar manualmente usandounsafe impl Send for MeuTipo {}eunsafe impl Sync for MeuTipo {}. Isso é inseguro porque você está afirmando que o tipo é seguro para concorrência, e o compilador não verificará. Use apenas se tiver certeza absoluta de que a implementação é segura, como em tipos que encapsulam primitivas atômicas.Qual a diferença entre
SendeSync? Dê um exemplo de um tipo que implementa ambos e um que implementa apenas um deles.✓ Resposta:Sendpermite transferir a propriedade entre threads;Syncpermite compartilhar referências entre threads. Um exemplo que implementa ambos éMutex<T>quandoT:Send. Um exemplo que implementa apenasSend(mas nãoSync) éCell<u32>.