我正在编写一些 tokio 异步代码。我有一个多索引数据结构,将用户保留在我想要使用粗粒度锁(与每个对象锁相反)保护他们的位置。我有这样的事情:
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 {}
如果末尾没有Send
and特征,编译器会抱怨不是and (合理地如此),因此通过 . 访问/修改是不安全的。Sync
RefCell<User>
Send
Sync
AsyncDb::add_user
我的解决方案是实现Send
和数据结构,因为周围Sync
有一个粗粒度锁,其中包含所述s。AsyncDb
UserDb
RefCell
这是正确的解决方案吗?它是否违反了任何不变量?有更好的方法来处理这个问题吗?
注意:这里是 Rust 初学者。我可能有很多概念上的差距,所以如果事情没有意义,请指出。
这几乎肯定是不合理的,除非即使在从用户读取数据时也只采用写锁。
这些
RefCell::borrow*()
函数不是线程安全的,因为它们不以原子方式维护内部引用计数。这意味着仅由读锁保护时使用borrow()
从 a 读取是不合理的。RefCell<User>
如果您已经对这个特定的设计感兴趣,我强烈建议您将
RwLock
其替换为Mutex
,因为读锁几乎完全没有用处。替换
RwLock
为Mutex
将使您的类型自动实现Sync
,但它仍然不会实现Send
。如果您unsafe impl Send
在这种情况下保留 s,那么您需要非常小心 sArc
。您绝不能:Arc<RefCell<User>>
值或对一个值的引用,或者嵌入到另一个值中,因为这将允许调用者克隆自己的值Arc
,他们可以使用它来操作RefCell
而不持有锁。Arc<RefCell<User>>
或对一个线程/任务的引用发送到另一个线程/任务。thread::spawn()
tokio::spawn()
tokio::spawn_blocking()
这种结构并不安全。
RefCell
不能同时在两个线程上使用(即它没有实现Sync
)并且您的结构无法防止这种情况。由于您使用 a
RwLock
,这意味着您可以为多个读取器提供对来自不同线程的值的不可变访问,这是不允许的。但即使您将其更改为Mutex
独占的,您的Arc
所有权也可以逃脱互斥锁的保护并在另一个线程中保留不可变的访问权限。话虽这么说,如果您确保没有逃脱的保护,那么实施
Send
和可能Sync
是有效的。不过,如果您这样做,您甚至可以将它们降级为s。是的,它必须是而不是因为,同样,您不能同时从多个线程访问 a,甚至是不可变的。Arc
Mutex
Rc
Mutex
RwLock
RefCell