☰
CyberRT 源码解析:一、高性能基础库 base(1)无锁哈希表 AtomicHashMap
2026/10/7 15:14:44 网站建设 项目流程
一、高性能基础库 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_hash_map.h

先记住一件事:Cyber 里到处在查「这个 ID 对应谁」。例如 Channel 号 → 缓冲区、节点号 → 名字。这些 ID 是整数,查得很勤,还经常好几个线程同时查、同时改。

std::unordered_map+ 一把大锁也能用,但锁一抢,大家都排队。AtomicHashMap的做法是:表很小、不扩容、不删除,用原子操作硬改指针。换来的是热路径上少排队。

先把几个最容易被问到的结论写死(后文展开):

  • 在 CyberRT 里干什么:给整数 ID(node / channel / service / task)做并发查找,例如GlobalData、DataDispatcher。
  • 支不支持并发读写:支持Set/Get/Has并发;没有 Delete。
  • 无锁怎么保证线程安全:不用 mutex,用std::atomic+ CAS(compare_exchange_strong),失败整轮重试。
  • 扩容和冲突:桶数编译期固定(默认 128,须为 2 的幂),不扩容;冲突用桶内有序拉链(链地址法),不是开放定址。

怎么用:

apollo::cyber::base::AtomicHashMap<int,std::string>map;map.Set(5,std::string("hello"));// 插入或覆盖std::string v;if(map.Has(5)&&map.Get(5,&v)){// v == "hello"}

(一)这张表长什么样

固定 128 个桶(2 的幂,用key & 127定位)。桶里是升序链表:哑头head_+ 若干Entry。value 不放在节点里,只挂指针,方便 CAS 换值。

图:Set(5,"hello")落到table_[5]。虚线框是哑头head_,实线框是有序Entry,绿色是堆上的 value。改值只 CAS 换指针。


(二)实现方法:定位、CAS、重试

无锁不是「不用同步」,而是不用互斥锁。同步收到 CAS 这一条不可打断的指令上。失败就整轮重来,系统整体还能推进。

对外入口很短:算桶,再交给桶上的Insert。

voidSet(K key,constV&value){uint64_tindex=key&mode_num_;// 5 & 127 → 5table_[index].Insert(key,value);}

Insert先Find,再 CAS。

Find在升序链上走,带回插入/更新要用的前后指针。链为哑头 → 3 → 7 → 10时:

找谁返回prevtarget接下来
7true37CAS 换value_ptr
5false37CAS 插到 3 和 7 中间
12false10空CAS 接到 10 后面

命中则换值:new一份V,CAS 把value_ptr从旧指针换成新的,成功就delete旧对象。

未命中则先new_entry->next = target,再 CASprev->next:从target换成new_entry。必须先接好后继,再挂到前驱,否则会断链。

CAS 失败不要死磕这一行:continue回到最外层while (true),重新Find。同一 key 并发写,最后一次成功的留下。


(三)读这段源码要用的 C++ 知识点

结构、步骤看完之后,剩下的卡点几乎都是语言知识,列出用到的以下四个知识点供大家参考。

(1)左值引用、右值引用

先分清两件事:表达式是左值还是右值,以及参数写成什么引用。

左值右值
直观有名字、能取地址,如变量s临时量、std::move(s)
引用写法T&/const T&T&&
这份代码Entry(K, const V&)拷贝Entry(K, V&&)尽量移动

容易混的点:

  • const int&是左值引用,int&&是右值引用。
  • int& a = b合法,当且仅当b是可修改的int左值;int& a = 10不合法。
  • const int&可以绑临时量;非 const 的int&不行。
  • 函数参数就算写成V&& value,有了名字之后,函数里的value是左值。所以要std::forward<V>(value)(或std::move)才能继续当右值用。

std::forward:按原来的值类别转发出去。std::move是无条件变成右值。这条V&&重载里两者效果接近;const V&那条不该写成forward<V>,拷贝和移动是两条路,不是繁简关系。

explicit Entry(K key):禁止Entry e = some_key这种隐式转换,只能显式构造。

默认构造new V()不是拷贝,只是造一个空的/零的V。对应Set(key)。

(2)compare_exchange_strong

签名可以记成:

boolcompare_exchange_strong(T&expected,T desired,memory_order success,memory_order failure);

语义:若当前值 ==expected,改成desired并返回 true;否则不改,把expected写成最新值,返回 false。比较和赋值中间插不进别人的写。

这份代码里两处 CAS:

  • 更新:value_ptr:old_val_ptr→new_value
  • 插入:prev->next:target→new_entry

有人抢先改了怎么办?返回 false,continue回到while (true)顶部,不是回到 CAS 那一行。插入失败即使没写continue,也会掉出else再进下一轮循环。

失败时expected会被覆盖——这是接口约定。这里失败后整轮Find,不依赖那个旧变量。

(3)memory_order这一组

原子性来自std::atomic的load/store/ CAS。memory_order_*只规定可见顺序。

关键字用在这份代码里的意思
releasestore对象构造完再公布指针
acquireload拿到指针后,能看见发布前的写入
acq_relCAS 成功又读又发:既看见别人,也让别人看见自己
relaxedCAS 失败没改出去,只要原子性

acquire不是「加载完再发布」,它是读端。发布是release。

memory_order_acquire也不会让一次普通赋值变成原子操作。写成int* p = q;再怎么写 memory_order 都没用。

成对记忆:store(ptr, release)↔load(acquire)。CAS 成功acq_rel、失败relaxed,是无锁代码的常见写法。

(4)std::atomic

std::atomic<V*>value_ptr={nullptr};std::atomic<Entry*>next={nullptr};

= {nullptr}是默认空指针。带 key 的构造函数里再store(new V(...), release)。

为什么 value 不直接做成V,而做成原子指针:换值时只 CAS 指针,不用拆节点。next同理,插入时 CAS 一条边。

atomic管的是指针这个字本身的读写;对象有没有构造完,要靠上面的release/acquire。


(四)核心函数:Has、Find、Insert

桶里就是一条按 key 升序的链表。Has/Find是查找,Insert是「找到了改值,没找到插节点」。无锁只多了两件事:指针用atomic读写,改链用 CAS,失败就整轮重找。

Insert只看右值版V&&。另外两个算法一样,一个拷贝、一个默认构造。

Has

// 判断链表中是否存在该节点boolHas(K key){// acquire:读到指针时,节点已经构造完Entry*m_target=head_->next.load(std::memory_order_acquire);while(Entry*target=m_target){// 空指针则到链尾,退出if(target->key<key){m_target=target->next.load(std::memory_order_acquire);continue;// 有序链,还没走到}else{returntarget->key==key;// 相等=有;更大=后面不可能再有}}returnfalse;}

有序链上走:比 key 小就继续;否则看等不等于。更大就可以停,后面不会再变小。

Find

// 找到 key 应插入位置的前驱 prev 和当前 targetboolFind(K key,Entry**prev_ptr,Entry**target_ptr){Entry*prev=head_;// 哑头,保证永远有前驱Entry*m_target=head_->next.load(std::memory_order_acquire);while(Entry*target=m_target){// 走到空则链尾if(target->key==key){*prev_ptr=prev;*target_ptr=target;returntrue;// 命中,后面改 value_ptr}elseif(target->key>key){*prev_ptr=prev;*target_ptr=target;returnfalse;// 应插在 prev 和 target 之间}else{prev=target;m_target=target->next.load(std::memory_order_acquire);}}*prev_ptr=prev;*target_ptr=nullptr;// 接到尾巴returnfalse;}

还是有序查找,多带回prev/target,给插入当挂钩。哑头head_保证永远有前驱。

  • 相等:找到了
  • 更大:应插在prev和target之间
  • 走到空:接到尾巴

Insert(K, V&&)

// 插入右值(少一次拷贝)voidInsert(K key,V&&value){Entry*prev=nullptr;Entry*target=nullptr;Entry*new_entry=nullptr;V*new_value=nullptr;while(true){// CAS 失败则整轮重找if(Find(key,&prev,&target)){// key 已存在:只换 value 指针if(!new_value){new_value=newV(std::forward<V>(value));// 移动,避免拷贝}autoold_val_ptr=target->value_ptr.load(std::memory_order_acquire);// CAS:若仍是 old,则换成 new;成功 acq_rel 发布,失败 relaxedif(target->value_ptr.compare_exchange_strong(old_val_ptr,// expected:期望仍是刚才读到的旧指针new_value,// desired:要换成的新 V*std::memory_order_acq_rel,// 成功:既看见别人,也让别人看见自己std::memory_order_relaxed)){// 失败:没改出去,只要原子性deleteold_val_ptr;// CAS 成功才删旧值if(new_entry){deletenew_entry;new_entry=nullptr;}return;}continue;// 有人抢先改了,回到 while 重新 Find}else{// key 不存在:链表插入if(!new_entry){new_entry=newEntry(key,value);}new_entry->next.store(target,std::memory_order_release);// 先接好后继// CAS:prev->next 仍是 target 才挂上 new_entryif(prev->next.compare_exchange_strong(target,// expected:挂钩还没被别人改过new_entry,// desired:新节点std::memory_order_acq_rel,std::memory_order_relaxed)){// 插入成功:prev → new_entry → targetif(new_value){deletenew_value;new_value=nullptr;}return;}// 有人抢先插了,下一轮重新 Find}}}

找到了:CAS 换value_ptr,等于改链表节点上的值。

没找到:先让新节点next指向target,再 CAS 把prev->next改成新节点——普通链表插入,只是用 CAS 抢这一步。

forward是为了移动而不是拷贝。new_value/new_entry只造一次,失败留着下一轮用。CAS 失败就回到while (true)重新Find,因为链可能已经变了。


常见问题

AtomicHashMap 在 CyberRT 里有什么作用?
给热路径上的「整型 ID → 对象」做查找:节点名、Channel、回调表。它是cyber/base里的容器,不是业务模块。

原理是什么?怎么实现无锁?
固定桶数组 + 桶内有序链表。改值 CASvalue_ptr,插节点 CASprev->next。失败就while (true)里重新Find。

相比普通哈希表好在哪?
不是单线程一定更快,而是少一把全局锁。多线程同时Set/Get时不必互相堵住。代价是不扩容、不删除、key 必须是整数。

base 库里它如何设计?源码细节有哪些?
AtomicHashMap只算桶下标;Bucket管链;Entry存key、原子value_ptr、原子next。对外Has/Get/Set,对内Find+Insert。详见上文(一)(四)。

高性能基础库里的无锁哈希表怎么用?
见文首示例:Set插入或覆盖,Has查询,Get取值。Get(key, V**)返回内部指针,不要delete,也不要拿太久。

支持并发读写吗?线程安全怎么保证?
Set/Get/Has可并发。安全靠原子指针和 CAS,不是读写锁。同一 key 同时写,最后一次 CAS 成功的值留下。

扩容和冲突处理是怎么做的?
不扩容。冲突走链地址法:同一下标进同一条升序链表。链太长只会变慢,不会自动 rehash。


这是博主第一次发中间件类笔记,用来记下自己学中间件的过程。Cyber RT 虽已发布多年,把热路径容器做成固定大小、无锁、按整数 ID 查找,这种取舍今天读起来仍值得学。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询