[面试题]C++中shared_ptr是线程安全的吗?
C++的智能指针有四种类型,去掉auto_ptr,主要有三种类型,定义在头文件中,用于管理动态分配的对象,并自动释放不再需要的对象,在C++中是部份线程安全的,但是并不意味着在在所有情况下都是安全的。
1.1 智能指针介绍
std::unique_ptr
表示独占式拥有,不允许多个指针指向一个资源;不支持复制,但是可以将所有权移动;这个智能指针最常用,开销是最小的;
std::unique_ptr<int> ptr1(new int(10));
// 或使用 make_unique(推荐)
auto ptr2 = std::make_unique<int>(10);
// 转移所有权
std::unique_ptr<int> ptr3 = std::move(ptr1);
std::shared_ptr
共享所有权,多个指针可以指向同一个资源,使用引用计数,当最后一个shared_ptr被销毁时,资源会被释放,且支持复制;后续会详细介绍;
std::weak_ptr
先看一段代码:
std::shared_ptr<int> shared = std::make_shared<int>(30);
std::weak_ptr<int> weak = shared;
if(auto locked = weak.lock())
{
std::cout << *locked << std::endl;
}
配合shared_pt
使用,不会增加引用计数,可以用来解决shared_ptr
循环引用的问题,可以通过lock()
方法获取shared_ptr
.
使用建议:
- 优先使用
unique_ptr
,除非确实需要共享所有权 - 创建智能指针时优先使用
make_unique/make_shared
而不是直接new
- 使用
weak_ptr
来避免循环引用 - 注意避免悬空指针(比如把原始指针存起来后智能指针释放了资源)
auto_ptr由于存在严重缺陷,不建议使用。在C++98引入,在C++17废除,存在严重缺陷,拷贝时会转移所有权,如下段代码:
// 不推荐使用
std::auto_ptr<int> p1(new int(10));
std::auto_ptr<int> p2 = p1; // p1变为空指针!
下面讨论智能指针的线程安全性:
std::unique_ptr
- 本身不是线程安全的
- 多个线程访问同一个
unique_ptr
需要加锁 unique_ptr
管理的资源的线程安全性取决于资源本身
std::shared_ptr
- 引用计数操作是线程安全的(内部使用原子操作)
- 但对指针指向的数据的访问不是线程安全的
std::weak_ptr
- 引用计数操作是线程安全的
- 数据访问不是线程安全的
// 如果需要线程安全,可以配合互斥锁使用
std::shared_ptr<int> ptr = std::make_shared<int>(42);
std::mutex mtx;
// 线程安全的访问
{
std::lock_guard<std::mutex> lock(mtx);
*ptr = 100;
}
对于shared_ptr
进行详细介绍:
2 shared_ptr
线程安全实战
2.1 shared_ptr
线程安全的部分
std::shared_ptr
的引用计数(即管理对象的共享所有权的计数)是线程安全的。因为std::shared_ptr
的内部引用计数是原子的,这意味着多个线程可以安全地对同一个 std::shared_ptr
对象进行引用计数的操作(如 shared_ptr
的拷贝构造和赋值)。这些操作不会导致数据竞争。
我们看个例子:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector
// 初始化一个共享的整型变量p,初始值为0
std::shared_ptr<int> p = std::make_shared<int>(0);
// 定义常量N,值为10000,用于确定数组大小
constexpr int N = 10000;
// 初始化两个共享指针数组,用于多线程环境下对共享资源的并发访问
std::vector<std::vector<std::shared_ptr<int>>> sp_arr1(N);
std::vector<std::vector<std::shared_ptr<int>>> sp_arr2(N);
/**
* 在给定的共享指针数组中增加元素
*
* @param sp_arr 要增加元素的共享指针数组
*/
void increment_count(std::vector<std::vector<std::shared_ptr<int>>>& sp_arr)
{
for (int i = 0; i < N; i++)
{
// 在数组的每个位置添加共享指针p
sp_arr[i].push_back(p);
}
}
int main() {
// 测试开始的提示信息
std::cout << "Shared_thread Test start============ \n " << std::endl;
// 创建两个线程,分别执行increment_count函数,传入不同的数组
std::thread t1(increment_count, std::ref(sp_arr1));
std::thread t2(increment_count, std::ref(sp_arr2));
// 等待两个线程执行完毕
t1.join();
t2.join();
// 输出共享指针p的引用计数
std::cout << "p.use_count() = " << p.use_count() << std::endl;
// 测试结束的提示信息
std::cout << " \n Shared_thread Test end=================" << std::endl;
return 0;
}
输出为:
Shared_thread Test start============
p.use_count() = 20001
Shared_thread Test end=================
初始引用计数:
p 最初有一个引用计数,因为它本身就是一个 std::shared_ptr
,所以初始引用计数为 1。
线程 t1 和 t2 的操作:
• 每个线程将 p 赋值给一个包含 10,000 个元素的向量中的每个元素。
• 每次赋值操作都会增加 p 的引用计数。
• 两个线程各自增加 10,000 次引用计数。
• 因此,总的引用计数增加量是 10,000 + 10,000 = 20,000。
最终引用计数:
初始引用计数 1 加上两个线程增加的引用计数 20,000,总计为 1 + 20,000 = 20,001。
关键在于每次赋值操作都会原子地增加引用计数。即使两个线程同时执行 sp_arr[i].push_back(p)
,也不会导致数据竞争或未定义行为。
2.2 线程不安全的部分
- 访问对象
如上面所说,std::shared_ptr
的引用计数是线程安全的,但对所管理对象的访问并不是线程安全的。如果多个线程同时访问同一个 shared_ptr
管理的对象,并且至少有一个线程在修改该对象,那么就需要额外的同步机制(如互斥锁)来确保线程安全。
#include <iostream>
#include <thread>
#include <vector>
std::shared_ptr<int> p1 = std::make_shared<int>(0);
void modify_memory()
{
for(int i = 0; i<10000; i++)
{
(*p1)++;
}
}
int main()
{
std::thread th1(modify_memory);
std::thread th2(modify_memory);
th1.join();
th2.join();
std::cout << "p1.use_count() = " << *p1 << std::endl;
return 0;
}
运行输出:
//第一次运行输出
p1.use_count() = 13615
//第二次运行输出
p1.use_count() = 12074
可以看到输出的结果不是预想的20000,每次输出结果都会发生变化。因此同时修改std::shared_ptr
指向的对象并不是线程安全的;
- 直接修改
std::shared_ptr
对象本身的指向
如果多个线程同时修改同一个std::shared_ptr
对象的指向,如赋值操作或重置操作,会导致数据竞争。
数据竞争可能导致以下问题:
引用计数的损坏:如果一个线程在修改 shared_ptr
的指向时,另一个线程也在修改它,可能会导致引用计数不一致,从而导致内存泄漏或双重释放。
未定义行为:访问已释放的内存或访问无效的指针。
#include <iostream>
#include <memory>
#include <thread>
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42); // 创建一个 shared_ptr
void modifySharedPtr() {
// 直接修改 shared_ptr 的指向
sharedPtr = std::make_shared<int>(100); // 不安全的操作
}
int main() {
std::thread t1(modifySharedPtr);
std::thread t2(modifySharedPtr);
t1.join();
t2.join();
std::cout << "Value: " << *sharedPtr << std::endl; // 可能导致未定义行为
return 0;
}
输出为:
Value: 100
多次运行上面的代码会发现,输出的value值有时候会是一个乱码数字,不是预期的100。为了避免这些问题,需要使用互斥锁(std::mutex
)来同步对sharedPtr
的修改。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42); // 创建一个 shared_ptr
std::mutex mtx; // 互斥锁
void modifySharedPtr() {
std::lock_guard<std::mutex> lock(mtx); // 加锁
sharedPtr = std::make_shared<int>(100); // 安全地修改指向
}
int main()
{
std::thread th1(modifySharedPtr);
std::thread th2(modifySharedPtr);
th1.join();
th2.join();
std::cout << "p1.use_count() = " << *p1 << std::endl;
return 0;
}
** 总结**
std::shared_ptr
的引用计数操作是线程安全的。- 对
std::shared_ptr
所指向的对象的访问需要额外的同步机制(如std::mutex)
来保证线程安全。 - 直接修改 std::shared_ptr 对象本身的指向在多线程环境中是不安全的,可能导致数据竞争和未定义行为。