我正在编写一些 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
和 Sync
特征,编译器会抱怨 RefCell<User>
不是 Send
和 Sync
(合理地如此),因此通过 AsyncDb::add_user
访问/修改不安全。
我的解决方案是为数据结构实现
Send
和 Sync
,因为 AsyncDb
中 UserDb
周围有一个粗粒度锁,其中包含所述 RefCell
。
这是正确的解决方案吗?它是否违反了任何不变量?有没有更好的方法来处理这个问题?
注意:这里是 Rust 初学者。我可能有很多概念上的差距,所以如果事情没有意义,请指出。
首先,我不确定为什么需要手动实现
Send
。当 RefCell<T>
也实现时,RwLock<T>
和 Send
都实现 T
,因此 AsyncDb
应自动实现 Send
。
为了解决您问题的实质,这几乎肯定是不听起来,除非您仅即使在从用户读取数据时也采用写锁。
RefCell::borrow*()
函数不是线程安全的,因为它们不以原子方式维护内部引用计数。这意味着仅由读锁保护时使用 borrow()
来读取 RefCell<User>
是不合理的。
如果您已经购买了此特定设计,我强烈建议您将
RwLock
替换为 Mutex
。