深井蛙
72.70M · 2026-03-07
目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day04 只做一件事:把控制块引用计数升级为原子,跑通多线程并发测试闭环。
今天要把 Day03 的控制块架构升级到线程安全版本:
std::atomic<long>(use/weak)fetch_add / fetch_sub / compare_exchange_weakrelaxed / acq_rel / acquireAddRefLock()(为 Day05 weak_ptr::lock 预埋)核心收获:理解 shared_ptr 的线程安全边界 + 无锁引用计数的关键实现点。
shared_ptr 的“线程安全”不是“所有操作都无锁安全”,而是很明确的承诺:
Reset(),另一个线程 get()/operator->,需要外部同步(mutex)。所以 Day04 的改动点非常聚焦:只改控制块计数与其操作,SharedPtr 本体保持“薄”。
关键文件(与你项目一致):
include/sp_counted_base.hinclude/sp_counted_impl.hinclude/shared_count.hinclude/my_shared_ptr.htest/test_thread_safe.ccinclude/sp_counted_base.h:
// 原子递增
inline void AtomicIncrement(std::atomic<long>* pw) {
pw->fetch_add(1, std::memory_order_relaxed);
}
// 原子递减 返回变化前的值
inline long AtomicDecrement(std::atomic<long>* pw) {
return pw->fetch_sub(1, std::memory_order_acq_rel);
}
// 条件递增:如果当前值非 0,则 +1;返回递增前的值
inline long AtomicConditionalIncrement(std::atomic<long>* pw) {
long r = pw->load(std::memory_order_relaxed);
while (true) {
if (r == 0) return r;
if (pw->compare_exchange_weak(
r, r + 1,
std::memory_order_relaxed,
std::memory_order_relaxed)) {
return r; // 成功:返回旧值
}
// 失败:r 会被更新为当前 *pw 的值,继续循环重试
}
}
要点:
fetch_add(relaxed):只需要计数原子性,不需要同步对象内容。
fetch_sub(acq_rel):最后一次 Release 可能触发 delete,需要建立同步。
compare_exchange_weak:
include/sp_counted_base.h:
class SpCountedBase {
protected:
std::atomic<long> use_count_;
std::atomic<long> weak_count_;
public:
SpCountedBase() : use_count_(1), weak_count_(1) {}
virtual ~SpCountedBase() noexcept = default;
virtual void Dispose() noexcept = 0;
virtual void Destroy() noexcept { delete this; }
void AddRefCopy() noexcept { AtomicIncrement(&use_count_); }
bool AddRefLock() noexcept {
return AtomicConditionalIncrement(&use_count_) != 0;
}
void Release() noexcept {
if (AtomicDecrement(&use_count_) == 1) {
Dispose();
WeakRelease();
}
}
void WeakAddRef() noexcept { AtomicIncrement(&weak_count_); }
void WeakRelease() noexcept {
if (AtomicDeincrement(&weak_count_) == 1) {
Destroy();
}
}
long use_count() const noexcept {
return use_count_.load(std::memory_order_acquire);
}
};
并发测试里写了:
threads.emplace_back([copy = std::move(copies[i])]() mutable {
copy.Reset();
});
这要求 SharedPtr 必须有 move ctor/assign,否则 std::move 只会退化成拷贝(rvalue 绑定到 const&),导致 copies[i] 仍然持有引用,test2 最终 use_count 卡住。
代码里已经补上了:
include/shared_count.hpp:
// move ctor
SharedCount(SharedCount&& other) noexcept : control_block_(other.control_block_) {
other.control_block_ = nullptr;
}
// move assign
SharedCount& operator=(SharedCount&& other) noexcept {
if (this != &other) {
if (control_block_) control_block_->Release();
control_block_ = other.control_block_;
other.control_block_ = nullptr;
}
return *this;
}
include/my_shared_ptr.hpp:
SharedPtr(SharedPtr&& other) noexcept
: ptr_(other.ptr_), count_(std::move(other.count_)) {
other.ptr_ = nullptr;
}
SharedPtr& operator=(SharedPtr&& other) noexcept {
if (this != &other) {
SharedPtr(std::move(other)).Swap(*this);
}
return *this;
}
要点:
ptr_ = nullptr,control_block_ = nullptr),否则析构路径会重复释放。SharedPtr(std::move(other)).Swap(*this) 是很稳的 move-assign 写法,最小改动、也更不容易写错资源交接顺序。test/test_thread_safe.cpp 覆盖了:
跑出来的最终输出已经证明:
原因:测试里使用了 std::move(copies[i]),但库侧如果缺 move ctor/assign,std::move 会退化成拷贝 → copies 仍持有那 20 份引用 → 最终 use_count 仍为 21。
修复:补齐 SharedCount/SharedPtr 的 move ctor/assign,并保证 moved-from 对象置空。
原因:测试里用了 C++14 的 init-capture([copy = ptr])。
解决方式:将 CMake 的 CMAKE_CXX_STANDARD 升级到 14,或改写测试为 C++11 捕获写法(更啰嗦,不推荐)。
因为引用计数会被多个线程同时修改。普通 ++/-- 是“读-改-写”三步,线程间交错会丢失更新:
std::atomic<long> 并用 RMW(fetch_add/fetch_sub)保证“每次增减都是原子动作”,才能支撑 shared_ptr 的并发拷贝/析构承诺。memory_order_relaxed:只保证原子性,不建立跨线程的可见性/顺序约束。适合“仅修改计数”的场景。AddRefCopy() 用 fetch_add(relaxed):只是把计数 +1,不需要同步对象内容。memory_order_acq_rel:成功的 RMW 同时具备 release + acquire 语义,用来建立关键 happens-before。Release() 的 fetch_sub(acq_rel):当它返回旧值为 1 时,当前线程即将执行 Dispose()/delete,必须确保 delete 与其他线程对对象的读写在内存模型上正确有序。两层原因:
*pw。compare_exchange_weak 允许“虚假失败”(spurious failure),即使值相等也可能返回 false。r)会被更新为当前真实值,下一轮可以直接用新值继续尝试。weak_ptr::lock() 的语义是:只有对象还活着(use_count != 0)时才能把弱引用提升为强引用。
如果用非原子的 “if (use_count != 0) ++use_count”,在竞态下可能出现:
AddRefLock() 用 CAS 把“检查 !=0”与“+1”合并成原子动作:要么成功提升,要么返回失败,绝不复活已销毁对象。Day04 已完成:线程安全引用计数闭环
std::atomic<long>)AtomicIncrement / AtomicDeincrement / AtomicConditionalIncrement 落地SpCountedBase::AddRefLock() 完成(为 weak_ptr::lock 预留)SharedCount/SharedPtr move 语义,跑通并发析构测试