在多线程环境下,使用标准库提供的 std::shared_ptr 和 std::weak_ptr 通常能满足大多数需求。然而,在一些高性能或特殊场景下,开发者可能需要对智能指针的行为进行细粒度的控制,例如自定义内存分配策略、延迟销毁、或更高效的原子计数实现。本文将以一种简化的自定义智能指针 SafeSharedPtr 为例,演示如何在 C++17/20 语义下实现线程安全的引用计数,并讨论可能的优化方向。
1. 设计目标
- 线程安全:对引用计数的读写必须是原子操作,防止数据竞争。
- 内存管理:支持自定义 deleter,并保证在最后一个引用释放时正确销毁对象。
- 轻量级:尽量减少额外的内存开销,避免在每个对象上存放完整的计数器。
- 可扩展:能够与标准库容器(如
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. 性能与优化
| 优化方向 | 说明 |
|---|---|
| 对齐与缓存行 | 把 strong 与 weak 放在同一缓存行,减少缓存行共享导致的写冲突。 |
| 分配器 | 对控制块使用 std::pmr::monotonic_buffer_resource 或自定义 pool,降低内存分配次数。 |
| 延迟计数 | 对于读多写少的场景,可在写操作前使用 std::mutex 保护计数,读操作使用原子。 |
与 std::atomic_ref 结合 |
在 C++20 可对已有计数器使用 atomic_ref,实现更灵活的引用计数。 |
5. 与 std::shared_ptr 的差异
- 计数器位置:标准实现将计数器与对象放在同一控制块;若想进一步压缩,可以将计数器拆分为
std::shared_ptr与std::weak_ptr的独立计数。 - 自定义 deleter:
SafeSharedPtr支持在构造时传入任意Deleter,可与资源管理(如文件句柄)无缝结合。 - 性能开销:在大多数情况下,标准库实现已经极为优化。自定义实现的主要价值在于需要特殊行为(如延迟销毁、事务性引用计数)时才更具优势。
6. 结语
实现一个线程安全的自定义智能指针,需要关注原子操作、内存可见性以及控制块的生命周期管理。通过上述 SafeSharedPtr 的示例,你可以在需要特殊行为或性能调优时快速替代 std::shared_ptr,并保持与标准容器的兼容性。记住,最重要的是在设计时考虑并发环境下的原子性与可见性,只有这样才能在多线程应用中稳如磐石。