导读:C++面试中有时会有这样一个问题,shared_ptr是线程安全的吗?对此问题,我们需要从三个并发场景进行考虑,拷贝shared_ptr的安全性、对shared_ptr赋值的安全性和读写shared_ptr指向内存区域的安全性。

对于以上问题,首先给出以下结论:

如果多个线程同时拷贝同一个shared_ptr对象,不会有问题,因为shared_ptr的引用计数是线程安全的。

如果多个线程同时修改同一个shared_ptr 对象,不是线程安全的。

如果多个线程同时读写shared_ptr指向的内存对象,不是线程安全的。

下面通过简单程序实验证明:

1. 引用计数更新,线程安全

这里我们讨论对shared_ptr进行拷贝的情况,由于此操作读写的是引用计数,而引用计数的更新是原子操作,因此这种情况是线程安全的。下面这个例子,两个线程同时对同一个shared_ptr进行拷贝,引用计数的值总是20001。

std::shared_ptr<int> p = std::make_shared<int>(0);
constexpr int N = 10000;
std::vector<std::shared_ptr<int>> sp_arr1(N);
std::vector<std::shared_ptr<int>> sp_arr2(N); void increment_count(std::vector<std::shared_ptr<int>>& sp_arr) {
for (int i = 0; i < N; i++) {
sp_arr[i] = p;
}
} std::thread t1(increment_count, std::ref(sp_arr1));
std::thread t2(increment_count, std::ref(sp_arr2));
t1.join();
t2.join();
std::cout<< p.use_count() << std::endl; // always 20001

2. 同时读写内存区域,线程不安全

下面这个例子,两个线程同时对同一个shared_ptr指向内存的值进行自增操作,最终的结果不是我们期望的20000。因此同时修改shared_ptr指向的内存区域不是线程安全的。


std::shared_ptr<int> p = std::make_shared<int>(0);
void modify_memory() {
for (int i = 0; i < 10000; i++) {
(*p)++;
}
} std::thread t1(modify_memory);
std::thread t2(modify_memory);
t1.join();
t2.join();
std::cout << "Final value of p: " << *p << std::endl; // possible result: 16171, not 20000

3. 直接修改shared_ptr对象本身的指向,线程不安全。

下面这个程序示例,两个线程同时修改同一个shared_ptr对象的指向,程序发生了异常终止。


std::shared_ptr<int> sp = std::make_shared<int>(1);
auto modify_sp_self = [&sp]() {
for (int i = 0; i < 1000000; ++i) {
sp = std::make_shared<int>(i);
}
}; std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(modify_sp_self);
}
for (auto& t : threads) {
t.join();
}

报错为:

pure virtual method called
terminate called without an active exception

用gdb查看函数调用栈,发现是在调用std::shared_ptr<int>::~shared_ptr()时出错,

(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007ffff7bc7859 in __GI_abort () at abort.c:79
#2 0x00007ffff7e73911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3 0x00007ffff7e7f38c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4 0x00007ffff7e7f3f7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5 0x00007ffff7e80155 in __cxa_pure_virtual () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6 0x00005555555576c2 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() ()
#7 0x00005555555572fd in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() ()
#8 0x0000555555557136 in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() ()
#9 0x000055555555781c in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::operator=(std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>&&) ()
#10 0x00005555555573d0 in std::shared_ptr<int>::operator=(std::shared_ptr<int>&&) ()
#11 0x000055555555639f in main::{lambda()#1}::operator()() const ()
...

其原因为:在并发修改的情况下,对正在析构的对象再次调用析构函数,导致了未定义的行为,从而发生了此异常。

对程序加锁后,程序可正常运行:

std::shared_ptr<int> sp = std::make_shared<int>(1);
std::mutex m;
auto modify = [&sp]() {
// make the program thread safe
std::lock_guard<std::mutex> lock(m);
for (int i = 0; i < 1000000; ++i) {
sp = std::make_shared<int>(i);
}
}; std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(modify);
}
for (auto& t : threads) {
t.join();
}
std::cout << *sp << std::endl; // running as expected, result: 999999

总结

  • shared_ptr未对指向的对象内存区域有线程安全保护,因此并发读写对应内存区域是不安全的。
  • 由于赋值操作涉及原内存释放、修改指针指向等多个修改操作,其过程不是原子操作,因此对shared_ptr进行并发赋值不是线程安全的。
  • 对shared_ptr进行并发拷贝,对数据指针和控制块指针仅进行读取并复制,然后对引用计数进行递增,而引用计数增加是原子操作。因此是线程安全的。

你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统、技术架构等计算机基础知识。如果你有任何问题或者建议,欢迎随时与我交流。如果这篇内容对您有帮助,希望能得到您的点赞和关注,之后将会持续分享更多干货。感谢阅读和支持!希望我们能一起探索程序员修炼之道。

C++ shared_ptr是线程安全的吗?的更多相关文章

  1. shared_ptr的线程安全

    1.9 再论shared_ptr 的线程安全 虽然我们借shared_ptr 来实现线程安全的对象释放,但是shared_ptr 本身不是100% 线程安全的.它的引用计数本身是安全且无锁的,但对象的 ...

  2. shared_ptr的线程安全性

    一: All member functions (including copy constructor and copy assignment) can be called by multiple t ...

  3. shared_ptr智能指针源码剖析

    (shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化.根据文档 (http://www.boost.org/doc/ ...

  4. 为什么多线程读写 shared_ptr 要加锁?

    https://www.cnblogs.com/Solstice/archive/2013/01/28/2879366.html 为什么多线程读写 shared_ptr 要加锁? 陈硕(giantch ...

  5. C++之shared_ptr总结

    转自 http://blog.csdn.net/u013696062/article/details/39665247 Share_ptr也是一种智能指针.类比于auto_ptr学习.所以推荐先学习a ...

  6. shared_ptr 和auto_ptr智能指针

    shared_ptr:计数的智能指针 它是一个包装了new操作符在堆上分配的动态对象,但它实现的是引用计数型的智能指针 ,可以被自由地拷贝和赋值,在任意的地方共享它,当没有代码使用(引用计数为0)它时 ...

  7. shared_ptr 用法

    引入 shared_ptr 是c++为了提高安全性而添加的智能指针,方便了内存管理. 特点 shared_ptr 是通过指针保持对象共享所有权的智能指针.多个 shared_ptr 对象可占有同一对象 ...

  8. 深入理解智能指针之shared_ptr(一)

    本文基于C++标准库源码分析shared_ptr,旨在搞清楚shared_ptr是什么,线程安全性等,目标能够安全的使用智能指针. (一)shared_ptr是一个类. 首先可以确定的是shared_ ...

  9. C++11智能指针

    今晚跟同学谈了一下智能指针,突然想要看一下C++11的智能指针的实现,因此下了这篇博文. 以下代码出自于VS2012 <memory> template<class _Ty> ...

  10. Boost使用笔记(Smart_ptr)

    我是Word写的,复制过来实在懒得在排版了,有兴趣的朋友可以去我的百度文库看,谢谢 http://wenku.baidu.com/view/34e485e2f61fb7360b4c653e.html ...

随机推荐

  1. 解决方案 | 如何解决subprocess.Popen(cmd)代码中含有空格路径的问题?

    一.背景 因为在python中需要用到subprocess.Popen(cmd),其中cmd由一堆连接的字符串构成:譬如,xxx.exe inputdir outputdir -arg1 -arg2 ...

  2. workman和swoole区别和异同

    swoole是使用C语言实现的socket通信框架,workerman则是使用纯php实现的socket框架,二者进程模型上也存在很多的不同. 先说下swoole的进程模型,看一下以下解析图 mast ...

  3. bitwarden本地搭建(无需购买SSL证书)

    bitwarden本地搭建(无需购买SSL证书) 在安装之前,笔者在这里先声明一下,我安装bitwarden使用的操作环境为ArchLinux,我的想法是,因为这只是一个"密码本" ...

  4. C#枚举高级应用

    文章开头先看一道题: 在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开 ...

  5. odoo Actions学习总结

    环境 odoo-14.0.post20221212.tar Actions(动作) action定义系统响应用户操作的行为:登录.操作按钮.选择发票等- action可以存储在数据库中,也可以作为字典 ...

  6. Figma 替代品 Excalidraw 安装和使用教程

    如今远程办公盛行,一个好用的在线白板工具对于团队协作至关重要.然而,市面上的大多数白板应用要么功能单一,要么操作复杂,难以满足用户的多样化需求.尤其是在进行头脑风暴.流程设计或产品原型绘制时,我们常常 ...

  7. 火山引擎VeDI数据技术分享:两个步骤,为Parquet降本提效

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 作者:王恩策.徐庆 火山引擎 LAS 团队 火山引擎数智平台 VeDI 是火山引擎推出的新一代企业数据智能平台,基 ...

  8. jpa+querydsl的平替国产easy-query最好用的orm

    jpa+querydsl的平替国产easy-query最好用的orm 一款国产最强java orm,完美支持可控强类型dsl,外加完美支持对象模型筛选拉取的orm,拥有非常智能的include(s)一 ...

  9. 4、SpringMVC之获取请求参数

    4.1 环境搭建 创建名为spring_mvc_demo2的新module,过程参考3.1节 4.1.1.创建请求控制器 package org.rain.controller; import org ...

  10. python语言绘图:绘制一组以beta分布为先验,以二项分布为似然的贝叶斯后验分布图

    代码源自: https://github.com/PacktPublishing/Bayesian-Analysis-with-Python ============================= ...