不能直接用 std::shared_mutex 配协程,因其 lock_shared() 和 lock() 是阻塞式调用,会挂起整个线程,违背协程“挂起当前协程、不阻塞线程”的设计目标;需自研基于原子状态机与等待队列的异步读写锁,核心状态为 idle/reader_count/writer_pending,通过 co_await 挂起协程并保证写优先级以避免饥饿。

为什么不能直接用 std::shared_mutex 配协程?
因为 std::shared_mutex 的 lock_shared() 和 lock() 是阻塞式调用,会挂起整个线程——而协程期望的是「挂起当前协程,让出执行权,不阻塞线程」。直接套用会导致线程级阻塞,彻底废掉协程的并发密度优势。
真正可行的路径是:自己维护等待队列 + 用 co_await 挂起协程 + 在锁释放时唤醒对应等待者。核心不是封装 mutex,而是实现一个可挂起/唤醒的异步同步原语。
如何设计一个支持协程挂起的读写锁状态机?
关键状态只有三个:idle(无人持有)、readers: N(N 个读协程持有)、writer_pending(有写请求在等,后续读请求需排队)。不能简单计数读锁,必须区分「活跃读」和「等待写」的优先级,否则会饿死写操作。
推荐结构:
- 用
std::atomic<int></int>存状态:正数 = 当前读协程数,0 = 空闲,-1 = 写者独占,-2 = 有写者在等(且无读持有) - 两个等待队列:
read_waiters(std::queue或std::vector)存std::coroutine_handle;write_waiters同理 - 所有状态变更和唤醒必须用原子操作 + 内存序(
memory_order_acq_rel最常用)
怎么写出可挂起的 async_lock_shared()?
它返回一个 awaitable 对象,该对象在无法立即获取读锁时挂起协程,并把 handle 加入 read_waiters;能获取时直接返回 true。
典型实现要点:
- 先用
compare_exchange_weak尝试将状态从idle (0)变为1,或从正数n变为n+1 - 若失败且当前状态为
-1(写者占用)或-2(写者在等),则入队并挂起 - 注意:入队后要再次检查状态——可能刚入队就有人释放了锁,避免漏唤醒
- await_ready() 返回 false 表示需挂起;await_suspend() 把 handle 存入队列;await_resume() 不做实际事,只让协程继续
示例片段(简化):
struct read_awaiter {
async_rwlock* rw;
bool await_ready() { return rw->try_acquire_read(); }
void await_suspend(std::coroutine_handle h) { rw->queue_read_waiter(h); }
void await_resume() {}
};
写锁的优先级和唤醒顺序怎么保证?
写锁必须「饥饿敏感」:一旦有写请求进入等待队列,后续新来的读请求就不能再抢入,否则写永远等不到机会。这是和传统读写锁最不同的地方。
具体逻辑:
-
async_lock()先尝试将状态设为-2(表示「写待命」),成功则入write_waiters并挂起 - 所有新
async_lock_shared()请求,若看到状态为-2或-1,一律入read_waiters(哪怕当前没写者在运行) - 释放写锁时,优先唤醒一个写等待者(如果还有);否则唤醒全部读等待者(批量唤醒更高效)
- 释放读锁时,仅当状态归零且
write_waiters非空,才唤醒一个写者——不能唤醒读,否则又绕回饥饿
容易被忽略的一点:唤醒写者后,必须把状态从 -2 改为 -1,否则其他写者无法再抢占;这个原子更新必须在唤醒前完成。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











