本文最后更新于2024-10-28,距今已有 593 天,若文章内容或图片链接失效,请留言反馈。
43_「C++」shared_ptr 的线程安全问题实验
「前言」文章内容大致是关于 shared_ptr 智能指针线程安全问题实验验证。
shared_ptr 是线程安全的吗?对此问题,需要从三个并发场景进行考虑:
- 拷贝 shared_ptr 的安全性。
- 对 shared_ptr 赋值的安全性。
- 读写 shared_ptr 指向内存区域的安全性。
一、引用计数更新(线程安全)
如果多个线程同时拷贝同一个 shared_ptr 对象(更新引体计数),不会有问题,因为 shared_ptr 的引用计数是线程安全的。
实验:三个线程同时对同一个 shared_ptr 进行拷贝,引用计数的值总是30001,符合预期。
#include <iostream>
#include <vector>
#include <thread>
#include <memory>
using namespace std;
const int N = 10000;
// shared_ptr<int> p(new int(0));
// std::make_shared是一个函数模板,用于创建一个std::shared_ptr智能指针并同时初始化它所指向的对象
// make_shared只进行一次内存分配。
// 当使用std::shared_ptr的常规构造函数(如std::shared_ptr<MyClass> ptr(new MyClass(5));)时,实际上会进行两次内存分配
shared_ptr<int> p = make_shared<int>(0);
void Count(vector<shared_ptr<int>>& sp_arr)
{
for (int i = 0; i < N; i++)
sp_arr[i] = p;
}
int main()
{
vector<shared_ptr<int>> sp_arr1(N);
vector<shared_ptr<int>> sp_arr2(N);
vector<shared_ptr<int>> sp_arr3(N);
// std::ref是一个函数模板,这个对象可以被看作是对原始对象的一个 “引用包装”,
// 主要用于在函数模板等场景中传递引用,特别是当函数模板的参数是按值传递,但你希望以引用的方式传递实际参数时。
thread t1(Count, std::ref(sp_arr1));
thread t2(Count, std::ref(sp_arr2));
thread t3(Count, std::ref(sp_arr3));
t1.join();
t2.join();
t3.join();
// 总共会有3 * N个shared_ptr指向p所指向的整数,再加上全局变量p本身,
// 引用计数为3 * N + 1,这里N为10000,所以输出总是30001
cout << p.use_count() << endl; // always 30001
return 0;
}
运行结果:
30001
二、并发读写 shared_ptr 对象指向的内存区域(线程不安全)
如果多个线程同时读写 shared_ptr 指向的内存对象,不是线程安全的。
实验:三个线程同时对同一个 shared_ptr 指向内存的值进行自增操作,最终的结果不是期望的 30000,则说明修改 shared_ptr 对象自身是线程不安全的。
#include <iostream>
#include <thread>
#include <memory>
using namespace std;
const int N = 10000;
shared_ptr<int> p = make_shared<int>(0);
void Modify()
{
for (int i = 0; i < N; i++) ++(*p);
}
int main()
{
thread t1(Modify);
thread t2(Modify);
thread t3(Modify);
t1.join();
t2.join();
t3.join();
// 预期是30000,p的初始化是0
// 结果是随机的,不是预期的
cout << *p << endl;
return 0;
}
运行结果是随机的
28374
注意:
- shared_ptr 只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题。
- 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。
- 指向堆上资源的线程安全问题是需要程序员自己加锁处理。
加锁之后就是符合预期了:
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>
using namespace std;
const int N = 10000;
shared_ptr<int> p = make_shared<int>(0);
mutex mtx;
void Modify()
{
for (int i = 0; i < N; i++)
{
mtx.lock();
++(*p);
mtx.unlock();
}
}
int main()
{
thread t1(Modify);
thread t2(Modify);
thread t3(Modify);
t1.join();
t2.join();
t3.join();
// 预期是30000,p的初始化是0
// 符合预期
cout << *p << endl;
return 0;
}
运行结果
30000
三、并发修改 shared_ptr 对象本身的指向(线程不安全)
如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。
实验:十个线程同时修改同一个 shared_ptr 对象的指向,程序发生了异常终止。
注:这个我在 Linux 上编译运行,可以正常运行,换成 VS2019 就触发异常,但是我这 VS2019 又无法显示调用的堆栈信息,好像是文件缺失无法显示,懒得整了,就截个异常终止的图吧。
#include <iostream>
#include <vector>
#include <thread>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp = make_shared<int>(10);
auto modifySelf = [&sp]() {
for (int i = 0; i < 100000; ++i)
{
// 我使用make_shared,演示不出效果,程序正常运行
shared_ptr<int> newsp(new int(i));
sp = newsp;
}
};
// 创建十个线程
vector<thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.push_back(thread(modifySelf));
}
for (auto& t : threads)
{
t.join();
}
return 0;
}
VS2019 运行结果

其原因为:在并发修改的情况下,对正在析构的对象再次调用析构函数,导致了未定义的行为,从而发生了此异常(多次析构)。
对程序加锁后,程序可正常运行:
#include <iostream>
#include <vector>
#include <thread>
#include <memory>
#include <mutex>
using namespace std;
mutex mtx;
int main()
{
shared_ptr<int> sp = make_shared<int>(10);
auto modifySelf = [&sp]() {
for (int i = 0; i < 100000; ++i)
{
lock_guard<mutex> lock(mtx);
shared_ptr<int> newsp(new int(i));
sp = newsp;
}
};
// 创建十个线程
vector<thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.push_back(thread(modifySelf));
}
for (auto& t : threads)
{
t.join();
}
return 0;
}
运行结果:正常运行结束。
四、总结
- 如果多个线程同时拷贝同一个 shared_ptr 对象(更新引体计数),不会有问题,因为 shared_ptr 的引用计数是线程安全的。
- 如果多个线程同时读写 shared_ptr 指向的内存对象,不是线程安全的。
- 如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。
再次强调:
- shared_ptr 只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题。
- 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。指向堆上资源的线程安全问题是需要程序员自己加锁处理。
- 修改同一个 shared_ptr 对象也是如此,需要程序员自己处理。
--------------- END ---------------
「 作者 」 枫叶先生
「 更新 」 2024.10.27
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。
43_「C++」shared_ptr 的线程安全问题实验
http://114.132.213.38:6250/archives/6c4c8caf-c78e-45ba-b62c-ca2a86400f06
评论