无锁编程的内存安全卫士:Folly危险指针实战指南
你是否在多线程编程中遇到过这样的困境:当一个线程正在读取某个对象时,另一个线程却将其删除,导致程序崩溃或数据损坏?作为开发者,我们需要一种机制来确保对象在被访问时不会被意外回收。Facebook开源的C++库Folly(folly/)提供了一种高效的解决方案——危险指针(Hazard Pointers)。本文将带你深入了解Folly危险指针的工作原理、使用方法以及在实际项目中的应用场景,帮助你轻松应对并发编程中的内存安全挑战。
危险指针:并发编程的内存安全保障
在并发编程中,无锁数据结构因其高性能而备受青睐,但随之而来的是内存回收的难题。危险指针是一种用于安全回收内存的技术,它通过让线程声明对对象的"兴趣",从而防止对象在被访问时被删除。
什么是危险指针?
危险指针(Hazard Pointer)是一种单写多读的指针,同一时间只能被一个线程拥有。当线程需要访问某个对象时,它会将该对象的地址存储在危险指针中,告诉其他线程:"我正在使用这个对象,请不要删除它!"
Folly的危险指针实现位于folly/synchronization/Hazptr.h头文件中。根据该文件的注释,危险指针的核心思想是:
危险指针是一种安全的内存回收方法。它保护对象在被一个或多个线程访问时不被回收,但允许对象在被访问的同时被并发删除。
为什么选择危险指针?
危险指针相比其他内存回收机制有以下优势:
- 高性能和可扩展性:危险指针的操作开销极低,通常在纳秒级别,适合高性能场景。
- 支持阻塞操作:与RCU(Read-Copy-Update)不同,危险指针允许在保护对象的同时进行阻塞操作。
- 内存使用可控:未回收对象的数量与危险指针的数量成线性关系,不会像RCU那样可能导致内存无限增长。
危险指针 vs 其他内存回收机制
Folly的危险指针实现提供了与其他内存回收机制的详细对比:
| 机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 锁(Locking) | 简单易懂 | 序列化操作,高开销,可能死锁 | 并发度低,简单场景 |
| 引用计数(如shared_ptr) | 自动回收,线程无关 | 读写都有高开销和竞争 | 线程局部支持不足,需要自动回收 |
| RCU | 简单,快速,可扩展 | 对阻塞敏感,不适合长时间操作 | 不需要在阻塞时保护对象 |
| 危险指针 | 高性能,支持阻塞,内存可控 | 实现相对复杂 | 高并发,需要长时间保护对象 |
Folly危险指针核心组件
Folly的危险指针实现包含多个核心组件,它们协同工作以提供安全高效的内存回收机制。
核心类与接口
Folly危险指针主要提供了以下核心类和接口:
- hazptr_holder:管理危险指针的持有者类,每个实例最多拥有一个危险指针。
- hazptr_obj_base:受保护对象的基类模板,提供retire()方法用于安全回收对象。
- protect():hazptr_holder的成员函数,用于保护原子指针指向的对象。
- retire():hazptr_obj_base的成员函数,用于标记对象为可回收,由危险指针系统在安全时自动回收。
这些组件的声明可以在folly/synchronization/Hazptr.h中找到,它们共同构成了危险指针的基本API。
优化的持有者类型
除了基本的hazptr_holder,Folly还提供了两种优化的持有者类型:
-
hazptr_array :管理M个危险指针的数组,相比多个单独的hazptr_holder,构造和析构速度更快(当M>1时)。
-
hazptr_local :提供更高的性能(构造/析构约2ns,而hazptr_holder约5ns),但限制当前线程在其存在期间不能创建其他持有者类型的对象。
这些优化类型在folly/synchronization/Hazptr.h的"Optimized Holders"部分有详细说明。
快速上手:Folly危险指针使用示例
让我们通过一个简单的示例来了解如何在实际项目中使用Folly危险指针。假设我们有一个配置类Config,需要在多线程环境下安全地读取和更新。
基本用法示例
首先,我们需要让Config类继承自hazptr_obj_base:
#include <folly/synchronization/Hazptr.h>
class Config : public folly::hazptr_obj_base<Config> {
public:
// 配置相关的成员函数
std::string getValue(const std::string& key) const {
// 实际的配置查询逻辑
return "value";
}
};
然后,我们可以使用危险指针来安全地访问和更新配置:
#include <atomic>
#include <folly/synchronization/Hazptr.h>
std::atomic<Config*> config_; // 原子指针存储当前配置
// 读取配置的函数
std::string getConfigValue(const std::string& key) {
folly::hazptr_holder h; // 创建危险指针持有者
Config* ptr = h.protect(config_); // 保护当前配置对象
return ptr->getValue(key); // 安全访问对象
// h析构时自动释放危险指针,不再保护该对象
}
// 更新配置的函数
void updateConfig(Config* newConfig) {
Config* oldConfig = config_.exchange(newConfig); // 原子交换配置指针
oldConfig->retire(); // 标记旧配置为可回收
}
在这个示例中,getConfigValue函数使用hazptr_holder来保护配置对象,确保在读取过程中对象不会被回收。updateConfig函数则使用retire()方法安全地标记旧配置为可回收,危险指针系统会在所有访问该对象的线程都结束后自动回收内存。
性能优化:使用hazptr_local
如果确定在访问配置的过程中不会使用其他危险指针,我们可以使用hazptr_local来获得更好的性能:
std::string getConfigValueOptimized(const std::string& key) {
folly::hazptr_local<1> h; // 创建优化的危险指针持有者
Config* ptr = h[0].protect(config_); // 通过数组访问危险指针
return ptr->getValue(key);
}
根据folly/synchronization/Hazptr.h的说明,使用hazptr_local可以将构造/析构时间从约5ns减少到约2ns,显著提升性能。
深入理解:危险指针的工作原理
要充分发挥危险指针的威力,我们需要深入理解其内部工作原理。Folly危险指针的实现涉及多个组件的协同工作,包括危险指针记录、线程本地存储、域管理等。
危险指针的生命周期
危险指针的生命周期可以分为以下几个阶段:
- 声明阶段:线程创建hazptr_holder对象,获取一个危险指针。
- 保护阶段:线程调用protect()方法,将危险指针指向要访问的对象。
- 使用阶段:线程安全地访问对象,其他线程无法回收该对象。
- 释放阶段:hazptr_holder析构,危险指针被重置,对象不再受保护。
- 回收阶段:当对象不再被任何危险指针指向时,由危险指针系统回收。
这个过程确保了对象在被访问期间不会被回收,同时在所有线程都结束访问后能够及时回收内存。
危险指针域(Domain)
Folly危险指针引入了"域"(Domain)的概念,用于对危险指针进行分组管理。默认情况下,所有危险指针都属于默认域,但用户也可以创建自定义域。
域的主要作用是:
- 隔离不同类型的对象回收,避免相互干扰。
- 允许针对不同类型的对象设置不同的回收策略。
自定义域的使用方法如下:
// 创建自定义危险指针域
folly::hazptr_domain my_domain;
// 使用自定义域的危险指针
folly::hazptr_holder h(&my_domain);
域的实现细节可以在folly/synchronization/HazptrDomain.h中找到(注:该文件未在当前目录列表中显示,但根据Hazptr.h的包含关系可以推断其存在)。
高级应用:保护链表结构
危险指针特别适合保护链表等复杂数据结构。Folly提供了专门的工具来简化链表的保护和自动回收。
链表节点的自动回收
Folly的folly/synchronization/HazptrObjLinked.h头文件提供了用于保护链表结构的工具。通过继承hazptr_obj_linked_base,我们可以创建支持自动回收的链表节点:
#include <folly/synchronization/HazptrObjLinked.h>
class ListNode : public folly::hazptr_obj_linked_base<ListNode> {
public:
int value;
std::atomic<ListNode*> next; // 原子指针指向下一个节点
ListNode(int val) : value(val), next(nullptr) {}
};
这种节点可以安全地用于无锁链表实现,危险指针系统会自动处理节点的回收。
遍历受保护的链表
使用危险指针遍历链表的示例代码如下:
folly::hazptr_holder h;
ListNode* head = ...; // 链表头节点
ListNode* current = h.protect(head);
while (current != nullptr) {
// 访问当前节点
process(current->value);
// 保护下一个节点,然后释放当前节点
ListNode* next = h.protect(current->next);
current = next;
}
这种遍历方式确保了即使在遍历过程中有节点被删除,也不会导致内存错误。
实际应用:Folly中的危险指针
Folly库内部广泛使用了危险指针来实现高效的无锁数据结构。例如,在folly/AtomicLinkedList.h中,危险指针被用于保护链表节点的访问和回收。
AtomicLinkedList的实现
AtomicLinkedList是Folly提供的一个无锁链表实现,其内部使用了危险指针来确保线程安全。以下是其部分实现代码:
template <typename T, bool MayContainNullptr = false>
class AtomicLinkedList {
// ... 其他成员 ...
template <typename... Args>
void emplace(Args&&... args) {
auto node = new Node(std::forward<Args>(args)...);
node->next_.store(head_, std::memory_order_relaxed);
while (!head_.compare_exchange_weak(
node->next_, node, std::memory_order_release, std::memory_order_relaxed)) {
}
}
// ... 其他成员函数 ...
};
虽然上述代码没有直接显示危险指针的使用,但Node类的实现很可能继承自hazptr_obj_base,从而利用危险指针进行安全的内存回收。
总结与展望
Folly危险指针为C++并发编程提供了一种高效、安全的内存回收方案。通过本文的介绍,我们了解了危险指针的基本概念、核心组件、使用方法以及在实际项目中的应用。
危险指针的优势
- 高性能:危险指针操作开销低,适合高性能场景。
- 灵活性:支持阻塞操作,适用范围更广。
- 内存安全:确保对象在被访问时不会被意外回收。
- 可扩展性:支持自定义域和优化的持有者类型。
未来展望
随着并发编程的普及,危险指针作为一种高效的内存回收机制,将会在更多场景中得到应用。Folly团队也在不断优化危险指针的实现,例如folly/synchronization/HazptrThreadPoolExecutor.h展示了危险指针在线程池中的应用,为更复杂的并发场景提供支持。
参考资料
- Folly官方文档:folly/
- 危险指针实现代码:folly/synchronization/Hazptr.h
- 链表节点支持:folly/synchronization/HazptrObjLinked.h
- 无锁数据结构:folly/AtomicLinkedList.h
希望本文能够帮助你更好地理解和应用Folly危险指针,解决并发编程中的内存安全问题。如果你有任何问题或建议,欢迎在评论区留言讨论!
提示:如果你觉得本文对你有帮助,请点赞、收藏并关注,以便获取更多关于Folly库和并发编程的优质内容。下期我们将深入探讨Folly中的无锁队列实现,敬请期待!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



