最近在使用 Rust 重写一些老项目时,经常会碰到 Arc、RwLock、Mutex 以及 RefCell 这些类型。它们都和“共享”或“可变”有关,这里简单整理一下它们的区别和常见使用场景。
前言
假设现在设计一个在线用户管理模块,并且需要支持通过编号获取对应的用户实例,看起来这个需求不复杂,但如果再往前走一步,就会发现后面很快会碰到共享所有权和内部状态修改的问题。
先看一个最简单的版本:
1 | pub struct User { |
一开始这样写没什么问题,UserMgr 负责保存用户,通过 id 取出来就行了。
Arc
1 | let mut user_mgr = UserMgr::new(); |
但用户对象通常不会只在 UserMgr 里停留。连接建立后,连接处理逻辑需要拿到它;收到消息后,消息分发逻辑也需要拿到它;后面的协议处理可能还会继续使用。
直接返回 &User 时,调用方只是借用了 UserMgr 中的用户。先看一个受借用规则限制的场景:
1 | let user = user_mgr.get(1001).unwrap(); |
这时可以让 UserMgr 保存 Arc<User>。其他模块拿到 Arc 后,只需要克隆一份 Arc,就可以共同持有同一个 User 实例。简单改造的版本:
1 | pub struct UserMgr { |
这次代码可以正常通过编译。remove 移除的只是 UserMgr 持有的那一份 Arc<User>,而调用方手中的 user 仍然持有另一份 Arc<User>。
这里的 .cloned() 并不会复制整个 User,它只会增加 Arc 的引用计数。只有最后一份 Arc<User> 被释放时,真正的 User 才会被销毁。
1 | let user = user_mgr.get(1001).unwrap(); |
到这里,Arc 解决的是“多个地方如何共同持有同一个用户”的问题。但当同一个 User 已经被多个 Arc 持有时,调用方无法直接取得 &mut User 来修改它的内部状态。
RwLock
User 不仅仅包含 id、email,还会有 name、current_project_id 等可能在用户在线期间被更新的状态字段。
一般来说,会这样设计:
1 | struct UserState { |
如果 User 只由一个地方持有,这样设计没有问题。但前面已经让多个模块通过 Arc<User> 共同持有同一个用户,此时调用方无法直接取得 &mut User,也就无法修改 state:
1 | let user = user_mgr.get(1001).unwrap(); |
对于这类需要共享、并且会频繁读取和偶尔修改的状态,可以使用 RwLock:
1 | pub struct User { |
update_name 通过 write() 获取写锁。写锁是独占的,在写锁释放之前,其他读取和写入 UserState 的操作都需要等待。
name 通过 read() 获取读锁。多个读操作可以同时持有读锁,因此当用户状态以读取为主、修改较少时,RwLock 可以让多个读取逻辑并发执行。
目前,这里的职责可以分成两层:
Arc<User>:让UserMgr、连接处理、消息分发等逻辑共同持有同一个User。RwLock<UserState>:让这些持有者能够安全地读取和修改用户的可变状态。
Mutex
这里使用 RwLock,是因为用户状态通常会被很多逻辑读取,而真正修改状态的操作相对较少。多个读取操作可以同时进行,只有在更新昵称、切换项目这类操作发生时,才需要独占写锁。
如果用户状态修改频繁,或者读写操作都很简单,使用 Mutex<UserState> 往往更直接:
1 | pub struct User { |
Mutex 不区分读取和修改。无论调用 name() 还是 update_name(),都需要先通过 lock() 获取互斥锁;同一时间只允许一个逻辑访问 UserState。
需要注意的是,无论是 RwLock 还是 Mutex,锁守卫的生命周期都决定了锁被持有的时间。实际使用时应尽量缩小持锁范围,不要在持有锁时执行耗时操作,尤其要避免跨 await 持有锁守卫。
RefCell
RwLock 和 Mutex 都用于多线程场景,但有些数据明确只会在单线程中使用,只是希望通过 &self 修改内部状态。这种情况下不需要加锁,可以使用 RefCell:
1 | pub struct User { |
RefCell 的用法和 RwLock 有些相似:borrow() 用于获取不可变借用,borrow_mut() 用于获取可变借用。它们返回的借用守卫离开作用域后,借用关系也会结束。
区别在于,RefCell 不提供线程安全保证。它将 Rust 原本在编译期检查的借用规则,放到了运行时检查。比如:
1 | let state = user.state.borrow(); |
因此,RefCell 适合单线程下的内部可变性。它不是 Mutex 或 RwLock 的轻量版本,不能用于多个线程共享的数据。
最后
Arc、Mutex、RwLock 和 RefCell 看起来都和“共享数据”有关,但它们解决的并不是同一个问题。
Arc 解决的是共享所有权:多个地方可以共同持有同一个值。
Mutex 和 RwLock 解决的是多线程下的内部可变性:当多个线程都可能访问同一份状态时,通过锁来约束读写顺序。
RefCell 同样提供内部可变性,但只适用于单线程场景。它会在运行时检查借用关系,而不是提供并发保护。