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

在多线程环境下,智能指针(如std::shared_ptr)通过引用计数机制实现资源共享与自动销毁。然而,默认的实现并不是线程安全的:若多个线程同时操作同一智能指针实例,引用计数的递增/递减可能出现竞争。为满足高并发需求,可自行实现一个线程安全的智能指针。下面给出一种常见的实现思路与关键细节。

1. 设计原则

  1. 数据独立:智能指针内部的数据结构(引用计数、控制块等)需要在多线程之间保持一致性。
  2. 轻量化:不希望在每一次复制、销毁时都产生额外的锁或内存开销。
  3. 可扩展:后续可对控制块进行额外扩展(如自定义析构、可观察对象等)。

2. 控制块(Control Block)

struct ControlBlock {
    std::atomic <size_t> use_count;  // 共享引用计数
    std::atomic <size_t> weak_count; // 弱引用计数
    void*          deleter;         // 自定义删除器
    void*          data;            // 原始指针
};
  • `std::atomic ` 用于保证递增/递减操作的原子性。
  • deleter 可以是 std::function<void(void*)> 或自定义函数指针。
  • data 存放真实对象指针。

3. 智能指针类

template <typename T>
class ThreadSafeSharedPtr {
public:
    explicit ThreadSafeSharedPtr(T* ptr = nullptr) {
        if (ptr) {
            ctrl_ = new ControlBlock{1, 0, nullptr, ptr};
        }
    }

    ThreadSafeSharedPtr(const ThreadSafeSharedPtr& other) noexcept {
        acquire(other.ctrl_);
    }

    ThreadSafeSharedPtr(ThreadSafeSharedPtr&& other) noexcept {
        ctrl_ = other.ctrl_;
        other.ctrl_ = nullptr;
    }

    ~ThreadSafeSharedPtr() {
        release();
    }

    ThreadSafeSharedPtr& operator=(const ThreadSafeSharedPtr& other) noexcept {
        if (this != &other) {
            release();
            acquire(other.ctrl_);
        }
        return *this;
    }

    ThreadSafeSharedPtr& operator=(ThreadSafeSharedPtr&& other) noexcept {
        if (this != &other) {
            release();
            ctrl_ = other.ctrl_;
            other.ctrl_ = nullptr;
        }
        return *this;
    }

    T* get() const noexcept { return ctrl_ ? static_cast<T*>(ctrl_->data) : nullptr; }
    T& operator*()  const noexcept { return *get(); }
    T* operator->() const noexcept { return get(); }

    size_t use_count() const noexcept { return ctrl_ ? ctrl_->use_count.load(std::memory_order_acquire) : 0; }
    explicit operator bool() const noexcept { return get() != nullptr; }

private:
    ControlBlock* ctrl_ = nullptr;

    void acquire(ControlBlock* cb) noexcept {
        ctrl_ = cb;
        if (ctrl_) ctrl_->use_count.fetch_add(1, std::memory_order_acq_rel);
    }

    void release() noexcept {
        if (ctrl_) {
            if (ctrl_->use_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
                // 最后一个共享引用,销毁对象
                if (ctrl_->deleter) {
                    // 自定义删除器
                    auto d = reinterpret_cast<void(*)(void*)>(ctrl_->deleter);
                    d(ctrl_->data);
                } else {
                    delete static_cast<T*>(ctrl_->data);
                }

                // 处理弱引用计数
                if (ctrl_->weak_count.load(std::memory_order_acquire) == 0) {
                    delete ctrl_;
                }
            }
            ctrl_ = nullptr;
        }
    }
};

关键点说明

  • 构造函数:若传入非空指针,则创建新的控制块,use_count 初始化为1。
  • 拷贝构造:调用 acquire,把计数加1。
  • 移动构造:直接转移控制块指针,避免计数变化。
  • 析构:调用 release,计数减1。若计数降到0,则销毁对象。随后检查弱引用计数,若为0,则删除控制块。
  • 计数操作:所有对计数的递增/递减都使用 std::atomic 并指定内存序保证可见性。

4. 线程安全性验证

#include <thread>
#include <iostream>
#include <vector>

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

int main() {
    ThreadSafeSharedPtr <Demo> ptr(new Demo(42));

    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        ThreadSafeSharedPtr <Demo> local = ptr;  // 拷贝
        threads.emplace_back([local]() mutable {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::cout << "Thread use_count: " << local.use_count() << "\n";
        });
    }

    for (auto& t : threads) t.join();
    std::cout << "Main finished\n";
}

运行时可观察到:

  • Demo 的构造一次,析构一次。
  • use_count 在每个线程中均保持正确,说明引用计数操作无竞争。

5. 可选扩展

  1. 弱引用:实现 `ThreadSafeWeakPtr `,维护 `weak_count` 并支持 `lock()`。
  2. 自定义分配器:将 ControlBlock 交由自定义分配器管理,减少碎片。
  3. 异常安全:在构造时若分配失败,确保不泄露资源。

6. 与 std::shared_ptr 的比较

  • 性能:自定义实现的 std::atomic 计数比标准库内部的 std::atomic 更透明,但可能略慢于编译器内置优化。
  • 功能:缺乏标准库的多功能性(如 enable_shared_from_thismake_shared 的优化)。
  • 可维护性:标准库经过多年实践与测试,稳定可靠;自定义实现需自行维护。

7. 结语

在需要自定义删除器或特殊内存管理策略的高并发项目中,手写线程安全的智能指针可以为你提供更细粒度的控制。通过上述示例,你可以在此基础上进一步完善,加入弱引用、内存池、或者更复杂的生命周期管理,以满足项目需求。

发表评论