先用代码测试下题目当中的情况(完整代码,可以直接复制用来测试,文末抽奖送书,欢迎参与)
VS2022 执行代码后,观察控制台输出:
Image
读线程持续打印 “Reader X reads: Updated Data”(初始值为空)。
写线程在测试期间未能成功获取锁,导致 shared_data 未被更新为 “Updated Data”
我这里代码故意在读处理中加了延时,读线程长时间持有锁,然后是 5 个读线程,可以发现写请求完全得不到机会来处理。
这个现象有个专业名词叫:写线程饥饿。
在多线程编程中,读写锁(Read-Write Lock)的机制是否会导致写请求在持续读请求下无法执行(即 写线程饥饿),取决于锁的具体实现策略和场景特性。以下是逐步分析:
一、读写锁的基本行为
读写锁的核心规则是 “读共享,写独占”:
- 读锁(共享锁):允许多个线程同时读取资源。
- 写锁(独占锁):同一时间仅允许一个线程写入资源,且写入时会阻塞所有读锁和写锁。
因此,当读锁持续占用时,写锁必须等待所有读锁释放后才能获取。但具体能否执行需结合锁的调度策略分析。
锁获取优先级:
无公平性策略:若读锁持续被获取,写锁可能无限等待(饥饿)。
写优先策略:当写锁请求存在时,后续读锁会被阻塞,直到写锁完成。
公平策略:交替服务读/写请求,避免单一方饥饿。
二、写请求能否执行的场景分析
场景 1:读写锁无公平性策略(常见默认实现)
问题:若读线程持续获取读锁(无间隙释放锁),写线程可能永远无法执行。
示例代码:
结果:写线程饥饿。
场景 2:读写锁支持写优先
策略:当有写锁等待时,新读锁请求被阻塞,直到写锁完成。
实现方式:维护写等待标记(如计数器),读锁获取前检查该标记。
结果:写线程最终能获得锁,但可能牺牲读吞吐量。
场景 3:读写锁支持公平性
策略:通过队列或时间戳保证读/写请求按到达顺序交替执行。
示例:Linux 内核的 rw_semaphore 使用公平队列。
结果:写线程不会饥饿,但并发读性能下降。
三、C++标准库 std::shared_mutex
我们文章开头的测试出现了写线程饥饿,那么 std::shared_mutex到底是公平性的还是非公平性的?还是说可以设置呢?
1. C++ 标准的立场
C++ 标准仅定义 std::shared_mutex 的接口和行为规范(如“读锁共享,写锁独占”),但 未规定锁的获取策略是否公平。这意味着:
公平性(如读/写锁的排队顺序、是否避免饥饿)由具体实现决定。
不同平台(如 Linux、Windows)或编译器(如 GCC、Clang、MSVC)可能有不同行为。
2. 常见实现的行为
Linux( GCC/libstdc++)
底层通常基于 pthread_rwlock_t,默认采用 读优先策略(允许新读请求抢占等待的写锁),可能导致 写线程饥饿。(不同 Linux 发行版或 glibc 版本可能有不同默认行为,需查阅具体文档)
示例:若某线程持有读锁时,其他读线程可以继续获取读锁,而写线程可能长时间无法获取锁。
Windows( MSVC)
底层可能使用 SRWLock(Slim Reader/Writer Lock),其特性是:无优先级保障的竞争式获取,可能但不必然导致写饥饿。 当锁释放时,等待的读/写线程通过竞争获取锁,不保证先到先得。
写线程可能因竞争失败而饥饿,但实际行为依赖线程调度。
四、解决方案:避免写饥饿的设计
1. 选择支持写优先的读写锁
手动实现(示例):
效果:当有写者等待时,新读者被阻塞,确保写者最终执行。
2. 使用操作系统级公平锁
Linux:通过 pthread_rwlockattr_setkind_np 设置 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP。
3. 业务层限流
在读逻辑中插入条件检查,主动释放锁允许写操作:(此方法依赖线程调度器实现,可能缓解但无法彻底避免饥饿)
六、总结
写请求能否执行:取决于锁实现的公平性策略。
无公平性策略 → 可能饥饿(需业务层干预)。
写优先或公平队列 → 可避免饥饿。