在多线程环境下使用智能指针时,最常见的问题是如何保证引用计数的原子性,从而避免数据竞争和悬空指针。下面通过一个完整的示例,演示如何使用 C++11/14/17 的原子操作和互斥锁,实现一个线程安全的 SharedPtr。
1. 设计思路
- 引用计数:使用
std::atomic<std::size_t>保存引用计数,保证自增、自减操作是原子性的。 - 控制块:每个
SharedPtr指向一个控制块(ControlBlock),控制块中存放引用计数和删除回调。 - 删除回调:使用模板化的
DeleteFunc,支持普通delete、delete[]以及自定义删除器。 - 线程安全:所有引用计数操作均使用原子操作;在
reset()、swap()等需要修改指针的操作时使用std::mutex或std::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. 关键点回顾
- 引用计数原子性:使用
std::atomic的fetch_add与fetch_sub,保证多线程环境下计数安全。 - 互斥锁:
reset与swap这类操作会修改内部指针或控制块指针,使用std::mutex防止并发冲突。 - 自定义删除器:模板化
Deleter使得SharedPtr能够支持数组、文件句柄等资源。 - 性能考虑:大多数操作(
operator*,operator->,use_count)不锁定,几乎不产生额外开销。
通过上述实现,你可以在自己的项目中直接使用 `SharedPtr
` 替代标准库中的 `std::shared_ptr`,并且保证在多线程环境下的安全性。若需要进一步优化,可考虑使用 `std::shared_ptr` 的 `use_count` 只读原子操作,或者使用更细粒度的锁策略。