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

在多线程环境下使用智能指针时,最常见的问题是如何保证引用计数的原子性,从而避免数据竞争和悬空指针。下面通过一个完整的示例,演示如何使用 C++11/14/17 的原子操作和互斥锁,实现一个线程安全的 SharedPtr

1. 设计思路

  • 引用计数:使用 std::atomic<std::size_t> 保存引用计数,保证自增、自减操作是原子性的。
  • 控制块:每个 SharedPtr 指向一个控制块(ControlBlock),控制块中存放引用计数和删除回调。
  • 删除回调:使用模板化的 DeleteFunc,支持普通 deletedelete[] 以及自定义删除器。
  • 线程安全:所有引用计数操作均使用原子操作;在 reset()swap() 等需要修改指针的操作时使用 std::mutexstd::lock_guard 进行互斥。

2. 控制块实现

#include <atomic>
#include <mutex>
#include <memory>

template<typename T, typename Deleter = std::default_delete<T>>
struct ControlBlock {
    std::atomic<std::size_t> ref_count{1};
    Deleter deleter;

    ControlBlock(Deleter d) : deleter(std::move(d)) {}
};

3. SharedPtr 基类

template<typename T, typename Deleter = std::default_delete<T>>
class SharedPtr {
public:
    using pointer = T*;
    using element_type = T;

    SharedPtr() noexcept : ptr_(nullptr), ctrl_(nullptr) {}
    explicit SharedPtr(T* ptr, Deleter d = Deleter()) noexcept
        : ptr_(ptr), ctrl_(new ControlBlock<T, Deleter>(std::move(d))) {}

    // 拷贝构造
    SharedPtr(const SharedPtr& other) noexcept {
        acquire(other.ptr_, other.ctrl_);
    }

    // 移动构造
    SharedPtr(SharedPtr&& other) noexcept {
        ptr_ = other.ptr_;
        ctrl_ = other.ctrl_;
        other.ptr_ = nullptr;
        other.ctrl_ = nullptr;
    }

    ~SharedPtr() {
        release();
    }

    // 拷贝赋值
    SharedPtr& operator=(const SharedPtr& rhs) noexcept {
        if (this != &rhs) {
            release();
            acquire(rhs.ptr_, rhs.ctrl_);
        }
        return *this;
    }

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

    // 访问
    T& operator*() const noexcept { return *ptr_; }
    T* operator->() const noexcept { return ptr_; }
    T* get() const noexcept { return ptr_; }
    std::size_t use_count() const noexcept { return ctrl_ ? ctrl_->ref_count.load() : 0; }
    explicit operator bool() const noexcept { return ptr_ != nullptr; }

    void reset(T* ptr = nullptr, Deleter d = Deleter()) noexcept {
        std::lock_guard<std::mutex> lock(mtx_);
        if (ptr_ != ptr) {
            release();
            if (ptr) {
                ptr_ = ptr;
                ctrl_ = new ControlBlock<T, Deleter>(std::move(d));
            } else {
                ptr_ = nullptr;
                ctrl_ = nullptr;
            }
        }
    }

    void swap(SharedPtr& other) noexcept {
        std::lock_guard<std::mutex> lock(mtx_);
        std::swap(ptr_, other.ptr_);
        std::swap(ctrl_, other.ctrl_);
    }

private:
    void acquire(pointer p, ControlBlock<T, Deleter>* c) noexcept {
        ptr_ = p;
        ctrl_ = c;
        if (ctrl_) ctrl_->ref_count.fetch_add(1, std::memory_order_relaxed);
    }

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

    pointer ptr_;
    ControlBlock<T, Deleter>* ctrl_;
    mutable std::mutex mtx_;  // 保护 reset 和 swap
};

4. 使用示例

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

void thread_func(SharedPtr <int> sp) {
    for (int i = 0; i < 1000; ++i) {
        // 读取
        int val = *sp;
        // 简单演示
        if (i % 200 == 0) std::cout << "Thread " << std::this_thread::get_id() << " reads " << val << '\n';
    }
}

int main() {
    auto sp = SharedPtr <int>(new int(42));

    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i)
        threads.emplace_back(thread_func, sp);

    for (auto& t : threads) t.join();

    std::cout << "Final use_count: " << sp.use_count() << '\n';
}

5. 关键点回顾

  1. 引用计数原子性:使用 std::atomicfetch_addfetch_sub,保证多线程环境下计数安全。
  2. 互斥锁resetswap 这类操作会修改内部指针或控制块指针,使用 std::mutex 防止并发冲突。
  3. 自定义删除器:模板化 Deleter 使得 SharedPtr 能够支持数组、文件句柄等资源。
  4. 性能考虑:大多数操作(operator*, operator->, use_count)不锁定,几乎不产生额外开销。

通过上述实现,你可以在自己的项目中直接使用 `SharedPtr

` 替代标准库中的 `std::shared_ptr`,并且保证在多线程环境下的安全性。若需要进一步优化,可考虑使用 `std::shared_ptr` 的 `use_count` 只读原子操作,或者使用更细粒度的锁策略。

发表评论