ReentrantMutex
主线位置:主线 1 的一个外包装。同一线程第二次
lock根本不碰RawMutex,只是计数加一;其他线程的lock走的仍是 RawMutex 的一字节状态机里那条完整的竞争路径。
flowchart TB
l["ReentrantMutex::lock"] --> id["id = get_thread_id.nonzero_thread_id()"]
id --> own{"owner == id?(Relaxed 读)"}
own -->|"是:本线程已持有"| inc["lock_count += 1(checked_add,溢出 panic)"]
own -->|"否"| raw["mutex.lock():普通 RawMutex,可能停车"]
raw --> set["owner = id;lock_count = 1"]
u["守卫 drop:unlock"] --> dec["lock_count -= 1"]
dec --> z{"变成 0?"}
z -->|"是"| clr["owner = 0;mutex.unlock()"]
z -->|"否"| keep["什么也不做"]
parking_lot 里所有的锁都不可重入:同一线程对 Mutex 加锁两次直接卡死,没有报错(坑 not-reentrant)。ReentrantMutex 是唯一的例外。它整个实现在 lock_api 里,parking_lot 只提供一个「怎么拿线程 id」的类型。
lock_api:两个字段加一把锁
lock_api/src/remutex.rs:64-149:
pub struct RawReentrantMutex<R, G> {
owner: AtomicUsize,
lock_count: Cell<usize>,
mutex: R,
get_thread_id: G,
}
#[inline]
fn lock_internal<F: FnOnce() -> bool>(&self, try_lock: F) -> bool {
let id = self.get_thread_id.nonzero_thread_id().get();
if self.owner.load(Ordering::Relaxed) == id {
self.lock_count.set(
self.lock_count
.get()
.checked_add(1)
.expect("ReentrantMutex lock count overflow"),
);
} else {
if !try_lock() {
return false;
}
self.owner.store(id, Ordering::Relaxed);
debug_assert_eq!(self.lock_count.get(), 0);
self.lock_count.set(1);
}
true
}
pub unsafe fn unlock(&self) {
let lock_count = self.lock_count.get() - 1;
self.lock_count.set(lock_count);
if lock_count == 0 {
self.owner.store(0, Ordering::Relaxed);
self.mutex.unlock();
}
}
owner为什么可以Relaxed:一个线程只会拿owner和自己的 id 比。读到等于自己的 id,只可能是自己之前写进去的(同一线程内的读写有程序顺序);读到别的值,不管是多旧的值,都不等于自己的 id,于是去走mutex.lock(),真正的同步由那把锁的Acquire/Release完成。lock_count是Cell<usize>,不是原子:只有持有锁的线程会读写它。RawReentrantMutex手写了Sync(71-72 行),正确性靠这条「只有持有者碰」的约定。- 计数溢出用
checked_add(..).expect(..):在 64 位上要嵌套 2⁶⁴ 次,实际只可能出现在mem::forget守卫的循环里;溢出时 panic「ReentrantMutex lock count overflow」,而不是回绕成 0。 try_lock、try_lock_for、try_lock_until都走同一个lock_internal,只是换了「真正加锁」的那个闭包(107-119、191-204 行)。本线程已持有时它们总是立即成功。
unlock 里的 - 1 没有检查:它是 unsafe fn,前提是调用方确实持有锁,守卫保证这一点。
parking_lot:线程局部变量的地址
pub struct RawThreadId;
unsafe impl GetThreadId for RawThreadId {
const INIT: RawThreadId = RawThreadId;
fn nonzero_thread_id(&self) -> NonZeroUsize {
// The address of a thread-local variable is guaranteed to be unique to the
// current thread, and is also guaranteed to be non-zero. The variable has to have a
// non-zero size to guarantee it has a unique address for each thread.
thread_local!(static KEY: u8 = 0);
KEY.with(|x| {
NonZeroUsize::new(x as *const _ as usize)
.expect("thread-local variable address is null")
})
}
}
源码没有说明为什么不用 std::thread::ThreadId;取一个线程局部变量的地址只需要一次 TLS 访问,推测这是选它的原因。注释里那句「必须非零大小」是关键:零大小类型的变量可以都放在同一个地址上。GetThreadId 的 safety 契约(35-55 行)只要求「同时活着的线程 id 互不相同」,已退出线程的 id 可以被复用,TLS 地址正好满足这一点。
ReentrantMutex<T> 是 lock_api::ReentrantMutex<RawMutex, RawThreadId, T>(src/remutex.rs:41)。RawThreadId 是零大小类型,RawMutex 1 字节,加上两个 usize,ReentrantMutex<()> 本机 24 字节、ReentrantMutex<u64> 32 字节(E1)。
守卫:只给 &T,永远 !Send
lock_api/src/remutex.rs:618-621:
pub struct ReentrantMutexGuard<'a, R: RawMutex, G: GetThreadId, T: ?Sized> {
remutex: &'a ReentrantMutex<R, G, T>,
marker: PhantomData<(&'a T, GuardNoSend)>,
}
两处和 MutexGuard 不同:
- 只实现
Deref,没有DerefMut。同一线程可以同时持有两个守卫,如果都能给&mut T,就有两个可变引用指向同一处。要改数据,里面套一个RefCell(文档原话「Use aRefCellif you need this」),借用冲突由RefCell在运行时报 panic。 - 标记写死成
GuardNoSend,不看R::GuardMarker。MutexGuard的Send性由RawMutex::GuardMarker决定,开了send_guardfeature 就是Send;可重入守卫无论如何都不是:守卫要是跑到别的线程去 drop,owner里记的还是原线程的 id,lock_count就乱了(GuardSend 与 send_guard)。
ReentrantMutex 的 Sync 只要求 T: Send,不要求 T: Sync(225-228 行):任一时刻只有一个线程拿得到 &T,所以 ReentrantMutex<RefCell<T>> 可以在线程之间共享。
公平解锁与 bump
unlock_fair 只在计数降到 0 时调用内层 mutex.unlock_fair()。bump(179-189 行)只在 lock_count == 1 时生效:暂存 owner,把 owner 和计数清零,调内层 bump(可能把锁交给别人再拿回来),再恢复 owner 和计数 1。嵌套持有(计数大于 1)时 bump 什么也不做,因为外层还有守卫指望锁一直在手里。这段计数处理是 lock_api 0.4.11 修过的(CHANGELOG「Fixed ReentrantMutex::bump lock count. (#390)」,提交 e23efd5「Reset lock_count appropriately in bump()」)。
什么时候用
本站几个卷里遇到过的「同线程重入自己的锁」:burn 反向传播重入自己那张图的锁(burn 卷,上游改成了 try_lock)、dioxus SyncStorage 拿着写锁再读(dioxus 卷)。这类问题改成 ReentrantMutex<RefCell<T>> 只是把死锁换成了 RefCell 的 already borrowed panic:借用冲突本身还在。它真正适合的是「同一线程的外层已经拿着锁、内层只读」或「回调可能重入但不会同时改」的场景;能缩小临界区、把回调挪到锁外,通常是更好的改法。
小结
RawReentrantMutex=owner: AtomicUsize+lock_count: Cell<usize>+ 一把普通RawMutex+ 取线程 id 的G;本线程已持有时只把计数加一,不碰内层锁。- parking_lot 的
RawThreadId用一个thread_local!里u8的地址当 id:非零,活着的线程之间唯一。 - 守卫只有
Deref,要改数据套RefCell;守卫恒为GuardNoSend,与send_guard无关。 - 计数溢出 panic;
bump只在计数为 1 时让出锁(lock_api 0.4.11,#390)。 - 本机
ReentrantMutex<()>24 字节。