目标:用 7 天时间,从“最简引用计数”迭代到接近 Boost shared_ptr 的控制块架构:默认删除器、自定义删除器、线程安全、weak_ptr、make_shared、最终工程化。
Day04 只做一件事:把控制块引用计数升级为原子,跑通多线程并发测试闭环。
1. 今日目标
今天要把 Day03 的控制块架构升级到线程安全版本:
- 控制块引用计数改为
std::atomic<long>(use/weak) - 引入原子操作:
fetch_add / fetch_sub / compare_exchange_weak - 选择合适的内存序:
relaxed / acq_rel / acquire - 实现
AddRefLock()(为 Day05 weak_ptr::lock 预埋) - 多线程并发测试验证:引用计数正确、对象只析构一次、无泄漏
核心收获:理解 shared_ptr 的线程安全边界 + 无锁引用计数的关键实现点。
2. 今日设计:shared_ptr 的线程安全边界
shared_ptr 的“线程安全”不是“所有操作都无锁安全”,而是很明确的承诺:
- ✅ 不同 shared_ptr 实例(共享同一控制块)可以在不同线程里并发拷贝/析构
因为它们只会并发修改控制块计数,计数原子化即可。 - ❌ 同一个 shared_ptr 实例被多个线程同时读写仍是数据竞争
例如一个线程Reset(),另一个线程get()/operator->,需要外部同步(mutex)。
所以 Day04 的改动点非常聚焦:只改控制块计数与其操作,SharedPtr 本体保持“薄”。
3. 关键实现
关键文件(与你项目一致):
include/sp_counted_base.hinclude/sp_counted_impl.hinclude/shared_count.hinclude/my_shared_ptr.htest/test_thread_safe.cc
3.1 原子操作封装:AtomicIncrement / AtomicDeincrement / AtomicConditionalIncrement
include/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:- expected 参数(这里的 r)失败时会被更新,所以循环能自动用最新值重试。
- weak 允许“虚假失败”,必须放循环。
3.2 控制块基类:SpCountedBase(原子 use/weak + AddRefLock)
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);
}
};
3.3 Day04 的“关键修复”:补齐 SharedCount / SharedPtr 的 move 语义
并发测试里写了:
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;
}
要点:
- move 必须把源对象置空(
ptr_ = nullptr,control_block_ = nullptr),否则析构路径会重复释放。 SharedPtr(std::move(other)).Swap(*this)是很稳的 move-assign 写法,最小改动、也更不容易写错资源交接顺序。
4. 单元测试:并发验证闭环
test/test_thread_safe.cpp 覆盖了:
- 并发拷贝
- 并发析构(你最关键的一关)
- 并发访问对象(不修改 shared_ptr 本身)
- 在线程间传递(按值传递)
- 引用计数压力测试
- 无内存泄漏验证(object_count 最终回到 0)
- 性能对比(单线程 vs 多线程)
跑出来的最终输出已经证明:
- test2 最终引用计数回到 1 ✅
- 对象数最终回到 0 ✅
- 全流程无断言失败 ✅
5. 开发过程中遇到的关键问题
问题 1:为什么 test2 最终 use_count 一直是 21?
原因:测试里使用了 std::move(copies[i]),但库侧如果缺 move ctor/assign,std::move 会退化成拷贝 → copies 仍持有那 20 份引用 → 最终 use_count 仍为 21。
修复:补齐 SharedCount/SharedPtr 的 move ctor/assign,并保证 moved-from 对象置空。
问题 2:为什么会出现 “lambda capture initializers only available with -std=c++14”?
原因:测试里用了 C++14 的 init-capture([copy = ptr])。
解决方式:将 CMake 的 CMAKE_CXX_STANDARD 升级到 14,或改写测试为 C++11 捕获写法(更啰嗦,不推荐)。
6. Day 04 学习检查清单
✅ 这一节就是你要求的“问题清单 = 今天四个检查清单”,并且给到可直接贴博客的详细回答。
Q1:理解为什么需要原子操作?
因为引用计数会被多个线程同时修改。普通 ++/-- 是“读-改-写”三步,线程间交错会丢失更新:
- 计数偏小 → 可能提前 delete(悬空指针/崩溃)
- 计数偏大 → 资源永远释放不了(泄漏)
- 更糟:重复释放(UB)
把 use/weak 计数改成std::atomic<long>并用 RMW(fetch_add/fetch_sub)保证“每次增减都是原子动作”,才能支撑 shared_ptr 的并发拷贝/析构承诺。
Q2:能解释 relaxed vs acq_rel 的区别?
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 与其他线程对对象的读写在内存模型上正确有序。
Q3:知道 compare_exchange_weak 为什么用在循环中?
两层原因:
- 并发竞争会导致 CAS 失败:别的线程可能在你读到 r 后修改了
*pw。 compare_exchange_weak允许“虚假失败”(spurious failure),即使值相等也可能返回 false。
因此标准写法必须是循环重试;并且 CAS 失败时 expected(这里的r)会被更新为当前真实值,下一轮可以直接用新值继续尝试。
Q4:理解 AddRefLock() 的作用(为 weak_ptr 准备)?
weak_ptr::lock() 的语义是:只有对象还活着(use_count != 0)时才能把弱引用提升为强引用。
如果用非原子的 “if (use_count != 0) ++use_count”,在竞态下可能出现:
- 读到 use_count==1
- 另一线程把最后一个 shared_ptr Release 到 0 并 delete
- 再 ++ 把计数“复活”为 1(但对象内存已释放)
这会直接 UB。
AddRefLock()用 CAS 把“检查 !=0”与“+1”合并成原子动作:要么成功提升,要么返回失败,绝不复活已销毁对象。
7. 今日迭代进度
✅ Day04 已完成:线程安全引用计数闭环
- 控制块 use/weak 计数原子化(
std::atomic<long>) AtomicIncrement / AtomicDeincrement / AtomicConditionalIncrement落地SpCountedBase::AddRefLock()完成(为 weak_ptr::lock 预留)- 补齐
SharedCount/SharedPtrmove 语义,跑通并发析构测试 - 多线程测试全通过:引用计数正确、对象只析构一次、无泄漏