**如何在C++中实现自定义智能指针以支持多线程安全?**

在多线程环境下,使用标准库提供的 std::shared_ptrstd::weak_ptr 通常能满足大多数需求。然而,在一些高性能或特殊场景下,开发者可能需要对智能指针的行为进行细粒度的控制,例如自定义内存分配策略、延迟销毁、或更高效的原子计数实现。本文将以一种简化的自定义智能指针 SafeSharedPtr 为例,演示如何在 C++17/20 语义下实现线程安全的引用计数,并讨论可能的优化方向。


1. 设计目标

  1. 线程安全:对引用计数的读写必须是原子操作,防止数据竞争。
  2. 内存管理:支持自定义 deleter,并保证在最后一个引用释放时正确销毁对象。
  3. 轻量级:尽量减少额外的内存开销,避免在每个对象上存放完整的计数器。
  4. 可扩展:能够与标准库容器(如 std::vector)无缝协作。

2. 关键组件

2.1 控制块(Control Block)

控制块保存引用计数、弱引用计数以及 deleter。示例代码如下:

template<typename T, typename Deleter = std::default_delete<T>>
struct ControlBlock {
    std::atomic<std::size_t> strong{1}; // 初始引用计数
    std::atomic<std::size_t> weak{0};   // 初始弱引用计数
    Deleter deleter;
    T* ptr;

    ControlBlock(T* p, Deleter d = Deleter{}) 
        : deleter(std::move(d)), ptr(p) {}
};
  • strong:强引用计数,控制对象生命周期。
  • weak:弱引用计数,确保控制块自身不被提前销毁。
  • deleter:自定义删除器。
  • ptr:指向实际对象。

2.2 SafeSharedPtr 实现

template<typename T, typename Deleter = std::default_delete<T>>
class SafeSharedPtr {
public:
    // 默认构造:空指针
    SafeSharedPtr() noexcept : cb(nullptr) {}

    // 从原始指针构造
    explicit SafeSharedPtr(T* p, Deleter d = Deleter{}) {
        if (p) cb = new ControlBlock<T, Deleter>(p, std::move(d));
    }

    // 拷贝构造
    SafeSharedPtr(const SafeSharedPtr& other) noexcept : cb(other.cb) {
        if (cb) cb->strong.fetch_add(1, std::memory_order_acq_rel);
    }

    // 移动构造
    SafeSharedPtr(SafeSharedPtr&& other) noexcept : cb(other.cb) {
        other.cb = nullptr;
    }

    // 析构
    ~SafeSharedPtr() { release(); }

    // 拷贝赋值
    SafeSharedPtr& operator=(const SafeSharedPtr& other) noexcept {
        if (this != &other) {
            release();
            cb = other.cb;
            if (cb) cb->strong.fetch_add(1, std::memory_order_acq_rel);
        }
        return *this;
    }

    // 移动赋值
    SafeSharedPtr& operator=(SafeSharedPtr&& other) noexcept {
        if (this != &other) {
            release();
            cb = other.cb;
            other.cb = nullptr;
        }
        return *this;
    }

    // 访问成员
    T& operator*() const noexcept { return *cb->ptr; }
    T* operator->() const noexcept { return cb->ptr; }
    T* get() const noexcept { return cb ? cb->ptr : nullptr; }

    // 当前强引用计数
    std::size_t use_count() const noexcept {
        return cb ? cb->strong.load(std::memory_order_relaxed) : 0;
    }

    explicit operator bool() const noexcept { return cb && cb->ptr; }

private:
    ControlBlock<T, Deleter>* cb;

    void release() noexcept {
        if (cb && cb->strong.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            // 唯一强引用被销毁,调用 deleter
            cb->deleter(cb->ptr);
            // 处理弱引用计数
            if (cb->weak.fetch_sub(1, std::memory_order_acq_rel) == 0) {
                delete cb; // 释放控制块
            }
        }
    }
};

2.3 线程安全要点

  • 原子计数:使用 std::atomic 并指定合适的 memory_order。在增删计数时使用 acq_rel 以保证强制顺序。
  • 控制块销毁:在 strong 计数归零后,先销毁对象,再递减 weak。当 weak 也归零时,安全地 delete 控制块。

3. 使用示例

struct Data { int x; };
int main() {
    SafeSharedPtr <Data> sp1(new Data{42});
    {
        SafeSharedPtr <Data> sp2 = sp1; // 计数 +1
        std::cout << "use_count: " << sp1.use_count() << '\n'; // 2
    } // sp2 离开作用域,计数 -1
    std::cout << "use_count: " << sp1.use_count() << '\n'; // 1

    return 0;
}

此例展示了 SafeSharedPtr 与标准 std::shared_ptr 在接口上的相似性。


4. 性能与优化

优化方向 说明
对齐与缓存行 strongweak 放在同一缓存行,减少缓存行共享导致的写冲突。
分配器 对控制块使用 std::pmr::monotonic_buffer_resource 或自定义 pool,降低内存分配次数。
延迟计数 对于读多写少的场景,可在写操作前使用 std::mutex 保护计数,读操作使用原子。
std::atomic_ref 结合 在 C++20 可对已有计数器使用 atomic_ref,实现更灵活的引用计数。

5. 与 std::shared_ptr 的差异

  • 计数器位置:标准实现将计数器与对象放在同一控制块;若想进一步压缩,可以将计数器拆分为 std::shared_ptrstd::weak_ptr 的独立计数。
  • 自定义 deleterSafeSharedPtr 支持在构造时传入任意 Deleter,可与资源管理(如文件句柄)无缝结合。
  • 性能开销:在大多数情况下,标准库实现已经极为优化。自定义实现的主要价值在于需要特殊行为(如延迟销毁、事务性引用计数)时才更具优势。

6. 结语

实现一个线程安全的自定义智能指针,需要关注原子操作、内存可见性以及控制块的生命周期管理。通过上述 SafeSharedPtr 的示例,你可以在需要特殊行为或性能调优时快速替代 std::shared_ptr,并保持与标准容器的兼容性。记住,最重要的是在设计时考虑并发环境下的原子性与可见性,只有这样才能在多线程应用中稳如磐石。

发表评论