news 2026/10/29 5:05:01

CyberRT 源码解析:一、高性能基础库 base(2)(3)原子读写锁与守卫

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CyberRT 源码解析:一、高性能基础库 base(2)(3)原子读写锁与守卫
一、高性能基础库 base ├── (1)无锁哈希表 AtomicHashMap ├── (2)原子读写锁 AtomicRWLock ← 本文 ├── (3)读写锁守卫 RWLockGuard ← 本文 ├── (4)可重入读写锁 ReentrantRWLock ├── (5)有界队列 BoundedQueue ├── (6)等待策略 WaitStrategy ├── (7)无界队列 UnboundedQueue ├── (8)线程安全队列 ThreadSafeQueue ├── (9)线程池 ThreadPool ├── (10)对象池 ObjectPool ├── (11)并发对象池 CCObjectPool ├── (12)信号槽 Signal ├── (13)区间遍历 FOR_EACH └── (14)基础宏 macros

源文件:cyber/base/atomic_rw_lock.h、cyber/base/rw_lock_guard.h

上一篇的哈希表自己改指针,表里面没有锁。Cyber 里还有调度器的协程表、拓扑图、Clock:普通容器,读的人多、改的人少。一把mutex会让读者也排队。下面这把锁允许好几个线程同时读,写的时候必须一个人独占。

先看最小用法。加锁、解锁你都不用手写,花括号结束就会解开:

usingapollo::cyber::base::AtomicRWLock;usingapollo::cyber::base::ReadLockGuard;usingapollo::cyber::base::WriteLockGuard;AtomicRWLock lock;// 默认写优先{ReadLockGuard<AtomicRWLock>g(lock);// 进来:加读锁// 读共享数据。别的线程也可以同时拿读锁}// 出去:自动解读锁{WriteLockGuard<AtomicRWLock>g(lock);// 进来:加写锁// 改共享数据。这时别人读、写都进不来}// 出去:自动解写锁

ReadLock()是私有的,所以你不能写lock.ReadLock()。为什么要私有、守卫里那几行在干什么,放到第(五)节。先搞清楚锁里那两个数字。


(一)两个数字:lock_num_和排队人数

锁的全部状态就是一个原子整数lock_num_,再加一个「有多少人正在抢写锁」的计数。常量只是起名:RW_LOCK_FREE = 0,WRITE_EXCLUSIVE = -1。

lock_num_名字含义别人可以做什么
0空闲没人持锁读者可以变成1;写者可以变成-1
1, 2, 3…读者人数几个线程正在读读者可以再+1;写者必须等到回到0
-1写者独占恰好一个线程在写读者、写者都要等

另外还有write_lock_wait_num_:已经执行了 WriteLock、但还没把lock_num_改成-1的人数。它不是「已经持有写锁」,只是报名。默认write_first_ = true:这个数大于 0,新读者就不许再+1。

对应到源码成员:

// 0表示没人占用锁staticconstint32_tRW_LOCK_FREE=0;// -1表示有人占用写锁staticconstint32_tWRITE_EXCLUSIVE=-1;// 最大重试次数staticconstuint32_tMAX_RETRY_TIMES=5;// ...// 等待写锁的线程数std::atomic<uint32_t>write_lock_wait_num_={0};// 锁的占用情况std::atomic<int32_t>lock_num_={0};// 是否优先写锁boolwrite_first_=true;

数字看懂之后,再用门口来记迁移(先有表,再有这句话):0是空屋子;正数是屋里几个读者;-1是一个写者把门锁上。写者进门前先在门口挂个号,write_lock_wait_num_就是挂了几个号。

这一节的问题

  • 正数为什么表示读者、要用-1表示写者?因为一个整数要同时表达「几个读者」和「有没有写者」。读者用加一减一;写者单独占一个负数,避免和「一个读者」的1混在一起。
  • 这把锁给谁用?调度器、拓扑仓库、Clock。哈希表自己 CAS,不用它。

(二)场景一:读者进门

默认写优先时,读锁是两层循环:内层等到「现在能读」,外层用 CAS 把人数加一。

inlinevoidAtomicRWLock::ReadLock(){uint32_tretry_times=0;int32_tlock_num=lock_num_.load();// 判断当前是否可读if(write_first_){do{// 如果锁的占用情况小于0或者等待写锁的线程数大于0,则等待while(lock_num<RW_LOCK_FREE||write_lock_wait_num_.load()>0){if(++retry_times==MAX_RETRY_TIMES){// 告诉操作系统让出CPU时间片std::this_thread::yield();retry_times=0;}lock_num=lock_num_.load();}// 代表没人占用写锁,可以占用读锁}while(!lock_num_.compare_exchange_weak(lock_num,lock_num+1,std::memory_order_acq_rel,std::memory_order_relaxed));}else{// 读优先do{while(lock_num<RW_LOCK_FREE){if(++retry_times==MAX_RETRY_TIMES){// saving cpustd::this_thread::yield();retry_times=0;}lock_num=lock_num_.load();}}while(!lock_num_.compare_exchange_weak(lock_num,lock_num+1,std::memory_order_acq_rel,std::memory_order_relaxed));}}

yield():当前线程对操作系统说「先跑别人」。不是把锁解开,也不是睡固定时间。

读优先(AtomicRWLock lock(false))内层只判断lock_num < 0,不管有没有人报名写。默认构造不走那条。

带数值走一遍:没有写者

假设现在lock_num_ = 2(两个读者),write_lock_wait_num_ = 0。线程 A 来拿读锁。

  1. lock_num读到2。
  2. 内层条件:2 < 0为假,wait > 0为假,不进内层 while。
  3. CAS:期望仍是2,想改成3。没人抢,成功。
  4. while (!true)为假,函数返回。现在lock_num_ = 3,三个读者。

CAS 可以先记成一句话:原子量还等于我手里的期望值,才改成新值。这里期望是局部变量lock_num(2),新值是3。

带数值走一遍:有人在抢写锁

同一时刻线程 B 已经进了WriteLock,报名成功,write_lock_wait_num_ = 1,但屋里还有读者,B 还没把数改成-1。线程 A 再来读。

  1. lock_num可能仍是2。
  2. 内层:wait > 0为真,卡在这一行的 while 里,反复load,满 5 次yield。
  3. A 不会去执行后面的 CAS,所以不会出现「写者在排队,读者人数还在涨」。
  4. 等 B 抢到-1再释放,wait回到 0 且lock_num_回到 0 之后,A 才能 CAS0 → 1。

这就是写优先:写者还没拿到锁,只报了名,新读者已经被挡住。否则读者可以一直+1,写者永远看不到0。

外层do-while还防一种情况:内层刚跳出,别人抢先把数改了。CAS 失败则!false为真,再进内层看门。

这一节的问题

  • 真正上读锁的是哪一句?最后的 CAS。前面的 while 只是等。
  • 线程 A 在写者报名时卡在哪?while (lock_num < 0 || wait > 0)。

(三)场景二:写者进门(报名是关键)

inlinevoidAtomicRWLock::WriteLock(){int32_trw_lock_free=RW_LOCK_FREE;uint32_tretry_times=0;// 等待写锁的线程数加1write_lock_wait_num_.fetch_add(1);// 尝试获取写锁while(!lock_num_.compare_exchange_weak(rw_lock_free,WRITE_EXCLUSIVE,std::memory_order_acq_rel,std::memory_order_relaxed)){// rw_lock_free will change after CAS fail, so init aginrw_lock_free=RW_LOCK_FREE;if(++retry_times==MAX_RETRY_TIMES){// saving cpustd::this_thread::yield();retry_times=0;}}// 等待写锁的线程数减1write_lock_wait_num_.fetch_sub(1);}

compare_exchange_weak(期望, 新值):

  • 成功:lock_num_从0变成-1,返回true。前面有!,while 结束。
  • 失败:原子量不变,但会把你传入的rw_lock_free改成当前真实值,返回false。

最后这一点对初学者最容易漏:失败改的是你手里那个局部变量,不是「把锁改坏」。所以循环里必须再赋成0,下次还是「我期望空闲」。

带数值走一遍

开始:lock_num_ = 2,wait = 0。线程 B 要写。

  1. wait变成1。之后新读者都会卡在上一节的内层 while。
  2. CAS:期望0,实际是2,失败。rw_lock_free被改成2。
  3. 若不写回0,下一轮会拿期望2去比,可能误把「两个读者」换成-1,把读锁拆掉。所以要rw_lock_free = 0。
  4. 两个读者陆续离开:2 → 1 → 0。
  5. B 再 CAS:0 → -1成功。wait减回0。B 独占。

报名为什么必须在 CAS 之前:若先抢0 → -1、抢不到再报名,中间会有空档,新读者可以继续+1,写者可能一直饿着。先报名,写优先才成立。

weak和哈希表里的strong都是 CAS。weak允许「值其实没变也失败一次」,这把锁外层本来就要重试,用weak即可;换成strong也对。假失败时同样会改期望值,同样要写回0。

这一节的问题

  • 为什么还没拿到锁,报名就能挡读者?因为读者看的是wait > 0,不是「已经是-1」。
  • while (!cas)何时为真?CAS 失败(屋里不空,或 weak 空失败)就继续转;成功才停。

(四)场景三:解锁

inlinevoidAtomicRWLock::ReadUnlock(){lock_num_.fetch_sub(1);}inlinevoidAtomicRWLock::WriteUnlock(){lock_num_.fetch_add(1);}

fetch_sub/fetch_add自己是原子的,不用再 CAS。

当前谁走之后为什么
3一个读者2人数减一
1最后一个读者0屋子空了,写者可以进
-1写者0-1 + 1 = 0,不是再减一

写者若也减一,会变成-2,读者会把「小于 0」一直当成有人在写,锁就坏了。

这四处才会改lock_num_:读 CAS 成功、写 CAS 成功、读解锁、写解锁。空转里的load只看不改。

同一线程再拿一次写锁:lock_num_已经是-1,自己再 CAS0 → -1永远失败,会空转等自己。这把锁不能重入,那是(4)ReentrantRWLock的事。


(五)守卫:为什么例子里要写ReadLockGuard

上面四句加解锁都是private。类开头写了友元,只有守卫能调:

friendclassReadLockGuard<AtomicRWLock>;friendclassWriteLockGuard<AtomicRWLock>;

所以不是「建议用守卫」,是只能用守卫。这也逼你把「加」和「解」绑在同一个栈对象上,避免ReadLock()之后return把ReadUnlock()跳过。

RAII:资源跟对象的寿命走。对象造出来就拿资源,对象销毁就还资源。

{ReadLockGuard<AtomicRWLock>g(lock);// 构造 → ReadLock()// ...}// 析构 → ReadUnlock(),即使中间 return 也会走这里

源码

template<typenameRWLock>classReadLockGuard{public:explicitReadLockGuard(RWLock&lock):rw_lock_(lock){rw_lock_.ReadLock();}~ReadLockGuard(){rw_lock_.ReadUnlock();}private:ReadLockGuard(constReadLockGuard&)=delete;ReadLockGuard&operator=(constReadLockGuard&)=delete;RWLock&rw_lock_;};
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 23:04:08

从 CDS 到 RAP,系统理解 ABAP Cloud 数据模型开发的核心能力

在 ABAP Cloud 项目里创建一个业务应用时,真正决定后续代码是否容易维护的,往往不是 Fiori 页面长什么样,也不是某个 OData 服务如何发布,而是底层数据模型是否设计得足够清晰。 一个典型的 RAP 应用可能从数据库表出发,向上构建 CDS view entity,再构建业务对象、投影视…

作者头像 李华
网站建设 2026/10/17 22:09:37

揭秘大数据生态圈:11大核心组件全解析

好程序员大数据培训在分享其内容之时, 专门就其生态系统进行了梳理, 本次我对此进行了整理, 因为所谓生态系统的概念, 指的就是一种以为平台的基础, 通过各类的应用框架相互之间保持兼容状态, 进而组成了一个具有独立性质的应用体系部分之人, 此类体系同样也被称作是生态圈这个…

作者头像 李华
网站建设 2026/10/20 10:34:23

Xen 与 Xvisor 虚拟化架构深度对比分析报告

摘要 在现代开源 Hypervisor(超监视器)领域,Xen 与 Xvisor 代表了两种截然不同的架构设计哲学。Xen 是经典的微内核(Microkernel-like)架构代表,通过特权虚拟机(Dom0)和拆分驱动模型(Split-Driver Model)构建起庞大的云计算虚拟化生态;而 Xvisor(Extensible Versat…

作者头像 李华
网站建设 2026/10/24 12:47:13

第56章 旅•旅人 一直在路上

终身成就奖之后的第二年春天&#xff0c;悦儿在整理办公室时无意间翻出了一张旧的登机牌。登机牌的纸张已经泛黄了&#xff0c;边缘有些卷曲&#xff0c;上面的印刷字体部分褪色&#xff0c;但仍能辨认出航班号和日期——2014年8月23日&#xff0c;巴黎飞往波士顿。她拿着那张登…

作者头像 李华
网站建设 2026/10/18 9:46:04

第43篇:常用工具-初始化自转——相机绕地心转,只有 Z 轴不翻滚,Y、X 轴 180° 就倒过来

大屏一开,地球自己悠悠地转起来——这大概是三维 GIS 项目里最常见、也最"理所当然"的一个需求。甲方不会追问它怎么实现的,只会问一句:“能不能转快一点?” 做自转不是第一次了,第 34 篇《地球自转》我们把相机钉进 ICRF 惯性系,让地球真的动起来;这次换个更…

作者头像 李华
网站建设 2026/10/13 19:34:58

Excel高频故障排查框架:从根因定位到彻底解决

很多人学Excel&#xff0c;其实一直都踩在同一个坑里&#xff1a;遇到问题就搜索&#xff0c;搜到答案就复制&#xff0c;复制完就忘。结果就是同一个类型的坑踩了七八遍&#xff0c;每次打开Excel还是心里发虚。这个系列第一天&#xff0c;我不想上来就倒给你一堆函数列表或者…

作者头像 李华