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

在现代 C++ 开发中,智能指针(std::unique_ptrstd::shared_ptrstd::weak_ptr)已成为管理资源的标准工具。它们在单线程环境下已能提供高效的内存管理与生命周期控制,但在多线程场景下,尤其是自定义智能指针时,常常需要考虑并发读写、原子计数以及线程间的同步开销。本文将从以下几个方面展开讨论,并给出一份可直接使用的自定义多线程安全智能指针实现示例。

1. 需求分析

我们希望实现一个类似 std::shared_ptr 的智能指针,但具备以下特性:

  1. 引用计数线程安全:计数的增减必须是原子操作,避免数据竞争。
  2. 延迟销毁:当计数归零后,资源需要安全地销毁,且不能被其他线程误用。
  3. 轻量化:尽量减少额外锁的使用,利用现代 CPU 的原子指令实现。
  4. 可扩展性:支持自定义的删除器(deleter)以及对对象内存的自定义对齐。

2. 关键技术点

2.1 原子计数(std::atomic<std::size_t>

std::atomic 提供了无锁的原子操作。我们将引用计数包装为 std::atomic<std::size_t>,使用 fetch_addfetch_sub 来安全地增减。

2.2 控制块(Control Block)

类似 std::shared_ptr,我们使用一个控制块来持有计数与删除器。控制块可以是一个 POD 结构,包含:

  • std::atomic<std::size_t> ref_count;
  • Deleter deleter;
  • void* ptr;

控制块本身会分配在堆上,指针指向它。

2.3 enable_shared_from_this 兼容

如果想让对象自己生成共享指针,需要继承 MySharedFromThis,类似 std::enable_shared_from_this。我们暂不实现此功能,以保持简洁。

3. 代码实现

下面是一份完整、可编译的实现示例,使用 C++17 及以上。

#pragma once
#include <atomic>
#include <memory>
#include <utility>
#include <type_traits>
#include <iostream>

namespace detail {

// 控制块基类,用于实现删除器多态
struct ControlBlockBase {
    std::atomic<std::size_t> ref_count{1};

    virtual void destroy(void* ptr) noexcept = 0;
    virtual ~ControlBlockBase() = default;
};

// 模板化的控制块,持有对象指针和删除器
template <typename T, typename Deleter>
struct ControlBlock : ControlBlockBase {
    T* ptr;
    Deleter deleter;

    ControlBlock(T* p, Deleter d)
        : ptr(p), deleter(std::move(d)) {}

    void destroy(void* /*unused*/) noexcept override {
        deleter(ptr);
    }
};

} // namespace detail

template <typename T, typename Deleter = std::default_delete<T>>
class MySharedPtr {
    using ControlBlockPtr = detail::ControlBlockBase*;

    T*       ptr_ = nullptr;
    ControlBlockPtr cb_ = nullptr;

public:
    // 默认构造
    constexpr MySharedPtr() noexcept = default;

    // 从裸指针构造,使用默认删除器
    explicit MySharedPtr(T* p, Deleter d = Deleter()) noexcept
        : ptr_(p) {
        if (p) {
            cb_ = new detail::ControlBlock<T, Deleter>(p, std::move(d));
        }
    }

    // 复制构造
    MySharedPtr(const MySharedPtr& other) noexcept
        : ptr_(other.ptr_), cb_(other.cb_) {
        add_ref();
    }

    // 移动构造
    MySharedPtr(MySharedPtr&& other) noexcept
        : ptr_(other.ptr_), cb_(other.cb_) {
        other.ptr_ = nullptr;
        other.cb_  = nullptr;
    }

    // 析构
    ~MySharedPtr() noexcept {
        release();
    }

    // 复制赋值
    MySharedPtr& operator=(const MySharedPtr& rhs) noexcept {
        if (this != &rhs) {
            release();
            ptr_ = rhs.ptr_;
            cb_  = rhs.cb_;
            add_ref();
        }
        return *this;
    }

    // 移动赋值
    MySharedPtr& operator=(MySharedPtr&& rhs) noexcept {
        if (this != &rhs) {
            release();
            ptr_ = rhs.ptr_;
            cb_  = rhs.cb_;
            rhs.ptr_ = nullptr;
            rhs.cb_  = nullptr;
        }
        return *this;
    }

    // 重载箭头与间接运算符
    T* operator->() const noexcept { return ptr_; }
    T& operator*() const noexcept { return *ptr_; }

    // 访问计数
    std::size_t use_count() const noexcept {
        return cb_ ? cb_->ref_count.load(std::memory_order_acquire) : 0;
    }

    explicit operator bool() const noexcept { return ptr_ != nullptr; }

private:
    void add_ref() noexcept {
        if (cb_) cb_->ref_count.fetch_add(1, std::memory_order_relaxed);
    }

    void release() noexcept {
        if (cb_ && cb_->ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
            cb_->destroy(ptr_);
            delete cb_;
        }
    }
};

3.1 关键点说明

  • 原子操作fetch_addfetch_sub 使用 std::memory_order_relaxed / acq_rel,确保计数操作无锁且正确。
  • 删除器多态:通过 ControlBlockBase::destroy 虚函数实现多态删除器,允许在构造时使用任何可调用对象。
  • 线程安全:计数变更完全由原子指令完成,读取 use_count 也使用 memory_order_acquire 以保证可见性。

4. 简单测试

#include "MySharedPtr.hpp"
#include <thread>
#include <vector>
#include <iostream>

struct Demo {
    int x;
    Demo(int v) : x(v) { std::cout << "Demo constructed\n"; }
    ~Demo() { std::cout << "Demo destroyed\n"; }
};

int main() {
    auto sp = MySharedPtr <Demo>(new Demo(42));
    std::cout << "use_count: " << sp.use_count() << '\n';

    // 通过多线程复制引用
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([sp](){
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::cout << "Thread " << std::this_thread::get_id() << " use_count: " << sp.use_count() << '\n';
        });
    }

    for (auto& t : threads) t.join();
    std::cout << "Main end, use_count: " << sp.use_count() << '\n';
}

运行结果会显示 Demo 在所有线程退出后被销毁,证明计数机制是线程安全的。

5. 性能与可扩展性

  • 无锁实现:所有计数操作均使用原子指令,避免了互斥锁带来的上下文切换开销。
  • 可定制删除器:用户可提供任何可调用对象(如 lambda、函数指针、functor)作为删除器,满足多种资源释放需求。
  • 内存占用:控制块为单独分配,且每个 MySharedPtr 只需 16 字节(指针 + 控制块指针),与标准库实现相当。

如果需要进一步优化,可考虑:

  • 对象池:对控制块使用对象池,减少频繁的 new/delete
  • 对齐与缓存行:将计数放在独立的缓存行,避免伪共享。
  • 弱引用实现:扩展为 MyWeakPtr,与 MySharedPtr 共用控制块。

6. 小结

本文展示了如何在 C++ 中实现一个线程安全的自定义智能指针。通过原子引用计数与多态删除器,实现了与 std::shared_ptr 类似的功能,且保持了轻量化与可扩展性。你可以直接将上述代码复制到项目中,根据需要进一步扩展 MyWeakPtr 或其他特性,满足多线程环境下的资源管理需求。

发表评论