无锁编程的内存安全卫士:Folly危险指针实战指南

无锁编程的内存安全卫士:Folly危险指针实战指南

【免费下载链接】folly An open-source C++ library developed and used at Facebook. 【免费下载链接】folly 项目地址: https://gitcode.com/GitHub_Trending/fol/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危险指针主要提供了以下核心类和接口:

  1. hazptr_holder:管理危险指针的持有者类,每个实例最多拥有一个危险指针。
  2. hazptr_obj_base:受保护对象的基类模板,提供retire()方法用于安全回收对象。
  3. protect():hazptr_holder的成员函数,用于保护原子指针指向的对象。
  4. 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危险指针的实现涉及多个组件的协同工作,包括危险指针记录、线程本地存储、域管理等。

危险指针的生命周期

危险指针的生命周期可以分为以下几个阶段:

  1. 声明阶段:线程创建hazptr_holder对象,获取一个危险指针。
  2. 保护阶段:线程调用protect()方法,将危险指针指向要访问的对象。
  3. 使用阶段:线程安全地访问对象,其他线程无法回收该对象。
  4. 释放阶段:hazptr_holder析构,危险指针被重置,对象不再受保护。
  5. 回收阶段:当对象不再被任何危险指针指向时,由危险指针系统回收。

这个过程确保了对象在被访问期间不会被回收,同时在所有线程都结束访问后能够及时回收内存。

危险指针域(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中的无锁队列实现,敬请期待!

【免费下载链接】folly An open-source C++ library developed and used at Facebook. 【免费下载链接】folly 项目地址: https://gitcode.com/GitHub_Trending/fol/folly

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值