在使用粗粒度锁访问的数据结构中使用 RefCell 是否安全?

问题描述 投票:0回答:2

我正在编写一些 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 初学者。我可能有很多概念上的差距,所以如果事情没有意义,请指出。

asynchronous rust concurrency traits rust-tokio
2个回答
1
投票

首先,我不确定为什么需要手动实现

Send
。当
RefCell<T>
也实现时,
RwLock<T>
Send
都实现
T
,因此
AsyncDb
应自动实现
Send

为了解决您问题的实质,这几乎肯定是听起来,除非您即使在从用户读取数据时也采用写锁。

RefCell::borrow*()
函数不是线程安全的,因为它们不以原子方式维护内部引用计数。这意味着仅由读锁保护时使用
borrow()
来读取
RefCell<User>
是不合理的。

如果您已经对这个特定的设计感兴趣,我强烈建议您将

RwLock
替换为
Mutex
,因为读锁几乎完全没用。

如果你这样做那个,你需要小心,不要将

Arc
暴露给这个结构的使用者。返回
Arc<RefCell<User>>
甚至
&Arc<_>
将允许调用者克隆他们自己的
Arc
,他们可以用它来操纵同样不健全的
RefCell


0
投票

不,这种结构不安全。

RefCell
不能同时在两个线程上使用(即它没有实现
Sync
),并且您的结构无法防止这种情况。

由于您使用了

RwLock
,这意味着您可以为多个读取器提供对来自不同线程的值的不可变访问,这是不允许的。但即使您将其更改为
Mutex
使其具有独占性,您的
Arc
也允许所有权逃脱互斥锁的保护并在另一个线程中保留不可变的访问权限。

话虽如此,如果您确保没有任何

Send
能够逃脱
Sync
的保护,则实施 Arc
Mutex
是“可能”有效的。但如果你这样做,你甚至可以将它们降级为
Rc
。是的,它必须是
Mutex
而不是
RwLock
,因为同样,您无法同时从多个线程访问
RefCell
,甚至是不可变的。
    

© www.soinside.com 2019 - 2024. All rights reserved.