在现代 C++ 开发中,智能指针(std::unique_ptr、std::shared_ptr、std::weak_ptr)已成为管理资源的标准工具。它们在单线程环境下已能提供高效的内存管理与生命周期控制,但在多线程场景下,尤其是自定义智能指针时,常常需要考虑并发读写、原子计数以及线程间的同步开销。本文将从以下几个方面展开讨论,并给出一份可直接使用的自定义多线程安全智能指针实现示例。
1. 需求分析
我们希望实现一个类似 std::shared_ptr 的智能指针,但具备以下特性:
- 引用计数线程安全:计数的增减必须是原子操作,避免数据竞争。
- 延迟销毁:当计数归零后,资源需要安全地销毁,且不能被其他线程误用。
- 轻量化:尽量减少额外锁的使用,利用现代 CPU 的原子指令实现。
- 可扩展性:支持自定义的删除器(deleter)以及对对象内存的自定义对齐。
2. 关键技术点
2.1 原子计数(std::atomic<std::size_t>)
std::atomic 提供了无锁的原子操作。我们将引用计数包装为 std::atomic<std::size_t>,使用 fetch_add 与 fetch_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_add与fetch_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 或其他特性,满足多线程环境下的资源管理需求。