文章目录
无锁ID管理链表
头文件:src/bthread/list_of_abafree_id.h
1 简介
BRPC 中用于实现 无锁(Lock-Free)且避免 ABA 问题的 ID 管理链表 的核心组件,主要用于在多线程高并发场景下高效、安全地分配和回收唯一标识符(如任务 ID、资源句 柄等 )。
2 设计实现说明
设计目标:提供一种无需锁竞争、无内存泄漏且防 ABA 问题的轻量级 ID 管理机制,支撑 BRPC 的高性能线程调度和资源管理。
核心设计思想:
-
ABA 问题规避
通过 标签指针(Tagged Pointer) 或 版本号(Versioning) 扩展指针的语义,确保每次修改链表节点时,指针的标签/版本号递增,从而避免 ABA 问题(即一个指针被释放后重新分配,但其值仍与旧值相同导致误判)。- 实现示例:使用
std::atomic<uint64_t>
将指针的低位存储地址,高位存储标签(如 16 位地址 + 48 位标签)。
- 实现示例:使用
-
无锁链表操作
使用原子操作(如compare_exchange_weak
)实现链表的插入、删除和遍历,确保线程安全且无阻塞。 -
ID 复用管理
维护一个空闲 ID 链表,支持快速分配和回收 ID,避免频繁的内存分配/释放操作。
3 关键数据结构与实现
3.1 链表节点结构
struct Node {
uint64_t id; // 唯一标识符(可能包含版本号)
std::atomic<Node*> next; // 带标签的原子指针
};
3.2 空闲链表管理
- 头指针:
std::atomic<Node*> _free_list
指向空闲链表的头部,通过原子操作实现并发访问。 - 分配 ID:从头节点取出一个 ID,并更新头指针。
- 回收 ID:将 ID 插回链表头部,并递增标签防止 ABA。
3.3 防 ABA 机制
- 标签指针:每次修改链表指针时递增标签值,确保即使地址复用,标签值不同也会导致 CAS 失败。
bool try_push(Node* new_node) { Node* old_head = _free_list.load(); new_node->next.store(old_head); return _free_list.compare_exchange_weak(old_head, new_node); }
4 核心 API 与功能
4.1 ID 分配
uint64_t allocate_id() {
Node* old_head = _free_list.load();
while (true) {
if (old_head == nullptr) {
return create_new_id(); // 扩展新 ID
}
Node* new_head = old_head->next.load();
if (_free_list.compare_exchange_weak(old_head, new_head)) {
return old_head->id; // 返回复用 ID
}
}
}
4.2 ID 回收
void release_id(uint64_t id) {
Node* node = reinterpret_cast<Node*>(id_to_node(id));
Node* old_head = _free_list.load();
do {
node->next.store(old_head);
} while (!_free_list.compare_exchange_weak(old_head, node));
}
4.3 链表扩展
void expand_free_list(size_t n) {
for (size_t i = 0; i < n; ++i) {
Node* node = new Node{generate_new_id(), nullptr};
release_id(node->id); // 将新生成的 ID 加入空闲链表
}
}
5 性能优化与特性
- 内存池化
预分配节点内存池,减少动态内存分配开销。 - 批量操作
支持批量分配和回收 ID,减少原子操作频率。 - 无锁设计
完全基于原子操作,避免锁竞争,适用于高并发场景。 - 标签溢出处理
标签值达到上限时重置或扩展位数,防止回绕问题。
6 应用场景
- 任务 ID 管理
为每个bthread
分配唯一 ID,用于调度和状态跟踪。 - 资源句柄池
管理网络连接、内存块等资源,支持快速分配和释放。 - 无锁队列索引
作为无锁队列的槽位索引,避免 ABA 导致的数据错误。
7 潜在问题与注意事项
- 标签位数限制
若标签位数不足(如 16 位),在高频回收场景下可能快速溢出,需合理设计标签长度。 - 内存对齐
标签指针需确保地址对齐,避免低位被占用(如 x86_64 通常要求 16 字节对齐)。 - 跨平台兼容性
不同平台对原子操作的实现可能不同,需适配(如 ARM 需内存屏障指令)。
8 示例代码片段
// 初始化空闲链表
std::atomic<Node*> _free_list{nullptr};
// 分配 ID
uint64_t id = allocate_id();
// 使用 ID
process_task(id);
// 回收 ID
release_id(id);
9 总结
list_of_abafree_id.h
提供了一种高效、无锁且防 ABA 的 ID 管理机制,是 BRPC 高并发能力的基石之一。其通过标签指针和原子操作实现了线程安全的 ID 分配与回收,适用于需要频繁创建和销毁资源的场景。开发者需关注标签溢出和平台兼容性,结合具体需求调整预分配策略和标签位数,以优化性能和稳定性。
10 延伸
10.1 ABA问题
ABA问题是在多线程编程中使用无锁数据结构时可能遇到的一个经典问题。
-
问题描述:
- 场景:线程A读取共享变量的值为A,准备用CAS(Compare-And-Swap)将其修改为B。
- 干扰:在线程A操作前,线程B将值改为B,随后线程C又将值改回A。
- 结果:线程A的CAS操作仍会成功(因当前值仍为A),但实际上变量经历了A→B→A的变化,导致逻辑错误。
-
危害:
- 数据不一致:如链表节点被释放后重用,可能导致指针指向无效内存。
- 逻辑错误:程序可能基于过期的上下文做出错误决策。
解决方案
- 标签指针(Tagged Pointers)
- 核心思想:将指针与版本号组合为原子变量,每次修改递增版本号。
- 实现示例:
#include <atomic> #include <cstdint> struct TaggedPointer { void* ptr; uintptr_t tag; }; class AtomicTaggedPointer { std::atomic<uintptr_t> value; static constexpr uintptr_t TAG_MASK = 0xFFFF000000000000; // 高16位为标签 static constexpr uintptr_t PTR_MASK = ~TAG_MASK; // 低48位为指针 public: TaggedPointer load() const { uintptr_t raw = value.load(); return {reinterpret_cast<void*>(raw & PTR_MASK), raw >> 48}; } bool compare_exchange(TaggedPointer& expected, void* new_ptr) { uintptr_t expected_raw = (expected.tag << 48) | reinterpret_cast<uintptr_t>(expected.ptr); uintptr_t desired_raw = ((expected.tag + 1) << 48) | reinterpret_cast<uintptr_t>(new_ptr); return value.compare_exchange_strong(expected_raw, desired_raw); } };
- 优点:轻量级,直接利用原子操作。
- 限制:标签位数有限(如16位),可能溢出。
- 风险指针(Hazard Pointers)
- 核心思想:线程声明其正在访问的指针,延迟释放内存直至无引用。
- 实现步骤:
- 注册风险指针:线程在访问共享资源前,将其指针存入线程本地列表。
- 延迟回收:释放内存时,先将其加入待回收队列,待所有线程的风险指针不再引用后安全删除。
- 示例库:
folly/Hazptr.h
(Facebook开源库)。
- 双字CAS(DCAS)
- 平台支持:利用如
CMPXCHG16B
指令(x86_64)实现128位原子操作。 - 实现:
#include <atomic> struct DoubleWord { void* ptr; uint64_t counter; }; std::atomic<DoubleWord> atomic_dw; bool atomic_update(DoubleWord& expected, void* new_ptr) { DoubleWord desired = {new_ptr, expected.counter + 1}; return atomic_dw.compare_exchange_strong(expected, desired); }
- 注意:需确保结构体对齐(
alignas(16)
),且在支持128位CAS的平台使用。
- 内存回收(Epoch-Based Reclamation)
- 核心思想:将内存释放延迟到所有线程退出当前操作纪元(Epoch)。
- 步骤:
- 进入纪元:线程在访问资源前标记当前纪元。
- 延迟释放:资源释放时,加入对应纪元的回收列表。
- 安全回收:当所有线程离开旧纪元后,回收其内存。
方案对比
方案 | 优点 | 缺点 |
---|---|---|
标签指针 | 实现简单,低延迟 | 标签溢出需处理 |
风险指针 | 避免标签溢出,内存安全 | 实现复杂,性能开销较大 |
双字CAS | 原子性强,无额外元数据 | 平台依赖,对齐要求高 |
纪元回收 | 适合批量回收,无标签限制 | 延迟回收可能导致内存占用高 |
ABA问题的解决需结合场景选择策略:
- 轻量级需求:标签指针(如BRPC的
list_of_abafree_id.h
)。 - 内存安全优先:风险指针或纪元回收。
- 平台支持:双字CAS在x86_64环境下高效可靠。
合理利用std::atomic
及其CAS操作,结合版本号或内存回收机制,可有效规避ABA问题,保障无锁数据结构的正确性。