Estou escrevendo um código assíncrono do Tokio. Eu tenho uma estrutura de dados de vários índices que mantém os usuários onde desejo protegê-los com um bloqueio de granulação grossa (ao contrário do bloqueio por objeto). Eu tenho algo assim:
use tokio::sync::RwLock;
struct User {
id: u64,
name: String,
}
// This class is not thread-safe.
struct UserDb {
by_id: HashMap<u64, Arc<RefCell<User>>>,
by_name: HashMap<String, Arc<RefCell<User>>>,
}
impl UserDb {
pub fn add_user(&mut self, name: String) -> Result<(), Error> {
// ...
}
}
// This class is thread-safe.
struct AsyncDb {
users: RwLock<UserDb>,
}
impl AsyncDb {
pub async fn add_user(&self, name: String) -> Result<(), Error> {
self.users.write().await.add_user(name)
}
}
// QUESTION: Are these safe?
unsafe impl Send for AsyncDb {}
unsafe impl Sync for AsyncDb {}
Sem características Send
e Sync
no final, o compilador reclama que RefCell<User>
não é Send
e Sync
(razoavelmente) e, portanto, não é seguro para acessar/modificar por meio de AsyncDb::add_user
.
Minha solução foi implementar Send
e Sync
para a estrutura de dados, uma vez que há um bloqueio de granulação grossa em AsyncDb
torno de UserDb
, que contém os referidos RefCell
s.
Esta é uma solução correta? Isso viola algum invariante? Existe uma maneira melhor de lidar com isso?
Nota: Rust iniciante aqui. Provavelmente tenho muitas lacunas conceituais, então, por favor, informe-as se as coisas não fizerem sentido.
É quase certo que isso não é correto, a menos que você use apenas bloqueios de gravação, mesmo ao ler os usuários.
As
RefCell::borrow*()
funções não são thread-safe porque não mantêm a contagem interna atomicamente. Isso significa que usarborrow()
para ler de umRefCell<User>
quando protegido apenas por um bloqueio de leitura não é adequado.Se você já comprou esse design específico, recomendo fortemente substituí-lo
RwLock
por umMutex
, pois os bloqueios de leitura serão quase completamente inúteis.Substituir
RwLock
porMutex
fará com que seu tipo seja implementado automaticamenteSync
, mas ainda assim não será implementadoSend
. Se você mantiver o seuunsafe impl Send
neste caso, precisará ter muito cuidado com oArc
s. Você nunca deve:Arc<RefCell<User>>
ou uma referência a um diretamente ou incorporada em outro valor, pois isso permitirá ao chamador clonar o seu próprio valorArc
, que poderá usar para manipularRefCell
sem manter um bloqueio.Arc<RefCell<User>>
referência ou para outro thread/tarefa viathread::spawn()
,tokio::spawn()
,tokio::spawn_blocking()
, enviando um em um canal e assim por diante.Esta estrutura não é segura.
RefCell
não pode ser usado em dois threads simultaneamente (ou seja, não implementaSync
) e sua estrutura não está protegendo contra isso.Como você usa a
RwLock
, isso significa que você pode fornecer a vários leitores acesso imutável ao valor de diferentes threads, o que não é permitido. Mas mesmo que você o altere paraMutex
exclusivo, vocêArc
permite que a propriedade escape da guarda do mutex e preserve o acesso imutável em outro thread, independentemente.Dito isto, implementar
Send
and provavelmenteSync
é válido se você garantir que nenhum escape da proteção do . Porém, se você fizer isso, provavelmente poderá até mesmo rebaixá-los para s. E sim, teria que ser e não porque, novamente, você não pode acessar a partir de vários threads ao mesmo tempo, mesmo de forma imutável.Arc
Mutex
Rc
Mutex
RwLock
RefCell