多线程环境下,调用不同的 shared_ptr 实例的成员函数是不需要额外的同步手段的,即使这些 shared_ptr 管理的是相同的对象。
多线程对于同一个 shared_ptr 实例的读操作(访问)可以保证线程安全;但对于同一个 shared_ptr 实例的写操作(改变一个 shared_ptr 指向的对象)则需要同步,否则会发生 race condition。即 shared_ptr 的引用计数本身是线程安全且无锁的,但 shared_ptr 对象本身的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化:
- 多个线程可以同时读取一个 shared_ptr 对象;
- 多个线程同时读写一个 shared_ptr 对象,则需要加锁。
以上讨论是 shared_ptr 对象本身的线程安全级别,而非其管理的对象的安全级别,shared_ptr 所管理的对象的并发操作是否为线程安全的,取决于具体所管理的对象,必要时需要使用一些同步手段加以保证。
shared_ptr 的数据结构
shared_ptr 是引用计数型智能指针,计数值保存在堆上动态分配的内存中。具体而言,shared_ptr<Foo>
包含两个成员,一个是 Foo 类型的指针,指向被管理的对象;一个 ref_count 指针,指向堆上的控制块:
![](https://wjiaman.fun/media/blog_image/sp0_bb14fcc3-adf1-417f-a5d2-439c8ab30a86.png)
由于 shared_ptr 间的拷贝涉及两个成员的复制,而这两步拷贝不会原子性地发生:
- 步骤 1:复制 ptr 指针:
![](https://wjiaman.fun/media/blog_image/sp3_9152cb8a-3db5-48b9-a16a-5888e19c0b11.png)
- 步骤 2:复制 ref_count 指针,并递增引用计数(此递增为线程安全的):
![](https://wjiaman.fun/media/blog_image/sp4_66caf23e-63f4-483e-9214-cf9aff1e5678.png)
多线程读 shared_ptr 是安全的
一个全局的 shared_ptr:
shared_ptr<Foo> global_ptr;
线程 1 到 N 运行:
void threadFunc(){
shared_ptr<Foo> local = global_ptr;
}
以上对于 shared_ptr 变量global
的读取操作是线程安全的。
多线程无保护读写 shared_ptr 可能出现的 race condition
而要写入一个 shared_ptr(即修改一个 shared_ptr 变量),由于涉及到对象的析构,则有可能带来 race condition。
考虑以下场景,有三个 shared_ptr:
shared_ptr<Foo> g(new Foo); // 线程间共享的 shared_ptr 对象
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
初始状态:
![](https://wjiaman.fun/media/blog_image/sp5_07e916af-46fb-42bc-aa87-14fc89701e54.png)
![](https://wjiaman.fun/media/blog_image/sp6_f541f13e-cae2-4b11-99e0-8e025806e30c.png)
- 先是步骤 1:
![](https://wjiaman.fun/media/blog_image/sp7_028b8bdf-16c7-4f2e-b05b-b31ae146bcd3.png)
- 再是步骤 2:
![](https://wjiaman.fun/media/blog_image/sp8_9f43d6ad-4455-47da-8c23-e72461427c98.png)
最后回到线程 A,完成步骤 2:
![](https://wjiaman.fun/media/blog_image/sp9_9d6b5777-f9f4-483d-b1ba-99ae0086ffaf.png)
当然 race condition 远不止这一种,其他线程交织还是可能造成其他错误。