C++ / a working model

39 / 80   ·   C++11   ·   约 11 分钟

shared_ptr:控制块与线程安全边界

先记住这句话

shared_ptr 共享的是一组销毁责任,通常通过控制块记录拥有者、删除器等信息。控制块允许不同 shared_ptr 实例并发管理同一对象,但不保护对象内部数据,也不允许无同步地修改同一个 shared_ptr 变量。

本篇内容
  1. 同一个地址不等于同一份所有权
  2. 存储指针与管理对象可以不同
  3. 线程安全有三层对象
  4. 运行示例
  5. 动手练习

同一个地址不等于同一份所有权

复制 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 的字段原子化。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录