39 / 80 · C++11 · 约 11 分钟
shared_ptr:控制块与线程安全边界
shared_ptr 共享的是一组销毁责任,通常通过控制块记录拥有者、删除器等信息。控制块允许不同 shared_ptr 实例并发管理同一对象,但不保护对象内部数据,也不允许无同步地修改同一个 shared_ptr 变量。
同一个地址不等于同一份所有权
复制 shared_ptr 会加入已有所有权组,最后一个强拥有者释放时调用约定的删除器。常见实现用控制块保存强弱计数、删除器和分配信息,但这些字段布局和 shared_ptr 的固定尺寸不是接口保证。不要根据调试器中的两个地址推导标准要求。
从同一个裸地址分别构造两个 shared_ptr,会建立两个互不知情的所有权组,通常导致重复删除。必须复制原来的 shared_ptr,或使用保留同一所有权组的转换。get 返回的地址只是观察结果,不能用它重新制造一份独立销毁责任。
存储指针与管理对象可以不同
别名构造可以让 shared_ptr 指向成员,同时保持整个宿主对象的生存期。示例中的字段指针指向 Bundle::value,但拥有的是 Bundle 所属的共享生命周期。外层 owner reset 后,字段指针仍保证宿主存活,直到它也 reset。
make_shared 常见地把对象和控制块合并分配,减少分配次数。最后一个强引用消失时对象仍按规则析构,但若还有 weak_ptr,控制块必须保留;合并分配的整块存储因此可能稍后才能归还。大型对象与长寿命弱观察者结合时应认识这一代价。
线程安全有三层对象
不同 shared_ptr 实例即使共享同一控制块,也可以在不同线程分别复制或 reset,不需要额外保护引用计数。同一个 shared_ptr 变量若被一个线程修改、另一个线程同时访问,则需要锁,或使用 C++20 的 atomic<shared_ptr<T>>。
第三层是 T 自身:两个合法 shared_ptr 同时写普通整数仍可能数据竞争。控制块只维护生存期,不替 T 加锁。use_count 在并发环境中只是瞬时观察,不能据此断言自己拥有独占修改权;示例的计数断言仅用于没有其他线程的确定场景。
容易答错的地方
const shared_ptr<T>只限制句柄,通常仍能修改 T;需要只读访问类型时使用 shared_ptr<const T>。- use_count 等于一不构成同步协议,另一个线程可能通过弱指针 lock 获得新的拥有者。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <memory>
struct Bundle {
static int alive;
int value;
Bundle() : value(42) { ++alive; }
~Bundle() { --alive; }
};
int Bundle::alive = 0;
int main() {
auto owner = std::make_shared<Bundle>();
std::shared_ptr<int> field(owner, &owner->value);
assert(owner.use_count() == 2);
owner.reset();
assert(Bundle::alive == 1);
assert(*field == 42);
field.reset();
assert(Bundle::alive == 0);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-shared-ptr.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
两个线程各持有 shared_ptr<Counter> 的副本,同时执行 ++p->value,引用计数安全是否足够?
查看参考答案
不够。两个句柄的控制块操作受到保证,但 value 若是普通整数,两次无同步写入产生数据竞争。可以给 Counter 的更新操作加同一把互斥锁,或把独立计数值设计成合适的原子类型。atomic<shared_ptr<Counter>> 只解决句柄发布与替换,并不会自动使 Counter 的字段原子化。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。