std::unordered_map不能直接用于多线程高频写入,因其无内置同步机制,桶数组与节点链接修改非原子;粗粒度互斥锁导致严重争用;无锁实现需cas更新头指针、惰性删除、原子表切换及纯函数哈希等严格约束。

为什么 std::unordered_map 不能直接用于多线程高频写入场景
因为它的内部结构(桶数组 + 链表/红黑树)在插入、扩容、删除时会修改共享指针和节点链接关系,std::unordered_map 本身不提供任何原子性保证。即使你用 std::mutex 包裹每次操作,也会因锁粒度太粗导致严重争用——尤其在写多读少或高并发插入时,吞吐量会断崖式下跌。
无锁设计的目标不是“完全不用锁”,而是把同步开销从临界区转移到单个原子指令上,靠 compare_exchange_weak 循环重试来推进状态变更。这意味着:所有共享数据必须是 POD 类型,节点内存布局要对齐,且不能有析构逻辑干扰原子更新。
- 扩容不能原地 realloc 桶数组,必须用原子指针切换新旧表(类似 Linux RCU 的 grace period 思路)
- 链表插入需用 CAS 更新头指针,失败就重试;删除则需标记节点为“逻辑删除”再惰性回收,避免 ABA 问题
-
std::atomic<node></node>是基础,但注意 x86 上对 16 字节指针的 CAS 需要cmpxchg16b支持,编译时得加-m128或改用 8 字节指针(如用索引代替裸指针)
如何用 std::atomic_ref + CAS 实现线程安全的桶头指针更新
每个桶是一个链表头,类型为 std::atomic<node></node>。插入新节点时,不能直接赋值,必须用循环 CAS 替换头指针:
Node* expected = bucket_head.load(std::memory_order_acquire);
do {
new_node->next = expected;
} while (!bucket_head.compare_exchange_weak(expected, new_node,
std::memory_order_release, std::memory_order_acquire));
这里的关键是 compare_exchange_weak 的失败重试逻辑——它比 strong 版本更轻量,适合循环场景;memory_order_acquire/release 足以保证链表节点可见性,无需全序屏障。
- 如果桶头指针是
nullptr,expected初始值就是nullptr,CAS 成功即完成插入 - 若多个线程同时插入,只有一个能成功更新头指针,其余线程拿到新
expected后继续重试,不会丢数据 - 切忌用
store()直接覆盖——这会破坏链表结构,导致节点丢失
扩容时如何避免读写冲突与内存泄漏
扩容不是“复制完再切换”,而是分三步原子推进:resize_flag 标记开始、new_table 原子发布、旧表惰性迁移。核心是用一个 std::atomic<table> 存当前活跃表指针,并在每次读写前检查是否需要迁移。<p>写操作伪代码逻辑如下:</p>
<pre class="brush:php;toolbar:false;">
Table* cur = table_ptr.load(std::memory_order_acquire);
if (cur->is_resizing()) {
migrate_one_node(key); // 将 key 对应桶的一个节点挪到新表
}
// 然后正常执行插入(在 cur 表上)
</pre>
<ul>
<li>迁移不是全量同步完成才切换,而是每次写操作顺带搬一个节点,摊平开销</li>
<li>新表分配必须用 <code>aligned_alloc(64, size),确保 Node 内存按缓存行对齐,避免 false sharing
为什么哈希函数必须是纯函数且禁止抛异常
无锁路径中任何非原子操作都不能被中断或回滚。如果 std::hash<t>::operator()</t> 内部调用了可能抛异常的函数(比如 std::string::hash 在某些实现里触发内存分配),整个 CAS 循环就会崩在中间——既没插入成功,也没释放资源,还卡住其他线程重试。
- 自定义 key 类型务必提供
constexpr哈希函数,例如用 FNV-1a 或 wyhash 的 header-only 实现 - 避免在哈希计算中访问虚函数、动态分配、锁、IO 或系统调用
- 对
std::string_view这类类型,哈希函数必须只读取其data()和size(),且保证生命周期长于哈希表操作
真正难的不是写通 CAS 循环,而是让整个内存模型下每个节点的生命周期、可见性、重排序边界都可预测。稍有不慎,就会出现查不到刚插入的 key,或者删掉不该删的节点——这种 bug 往往只在特定核数、特定负载下偶发,复现成本极高。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











