基础代码
t1线程首先执行 DoIt ,并将链表最后一个元素传入进去,然后轮询is_l,
t2线程首先休眠3秒(保证1线程先跑),然后将链表中值为3和5的元素删除,置is_l为1。
t1此时轮询结束,查看i1的值,由于t2已经将5给删除了,那么t1在这之后访问值为5的元素还能成功吗?
bool is_l = false;
void DoIt(std::shared_ptr<int> &i1) {
while (!is_l) {
}
std::cout << "loop: " << i1 << std::endl; // loop: 0x562e787763b0
std::cout << *i1 << std::endl;
}
/** IS, IS, X integration test */
void MyPointTest() {
// 定义一个 全局链表
std::list<std::shared_ptr<int>> l1;
// 插入数据
l1.push_back(std::make_shared<int>(1));
l1.push_back(std::make_shared<int>(2));
l1.push_back(std::make_shared<int>(3));
l1.push_back(std::make_shared<int>(4));
l1.push_back(std::make_shared<int>(5));
// 定义两个线程
std::thread t_1([&]{
DoIt(l1.back());
});
std::thread t_2([&]{
// 线程2 休眠一会,然后再启动并且执行一些操作
std::this_thread::sleep_for(std::chrono::seconds(3));
for(auto it=l1.begin();it!=l1.end();++it){
if(**it==3){
l1.erase(it);
break;
}
}
for(auto it=l1.begin();it!=l1.end();++it){
if(**it==5){
std::cout << "5: " << *it << std::endl; // 5: 0x562e787763b0
l1.erase(it);
break;
}
}
is_l = true;
});
t_1.join();
t_2.join();
}
TEST(LockManagerTest, PointTest) { MyPointTest(); } // NOLINT
运行结果
DoIt 函数输出了 5 ,这表示另一个线程销毁了这个智能指针之后,这个智能指针的其他引用还能正常访问到这个地址。
这里就得到一个初略的 结论,那么下面我们测试一下,这里的智能指针的引用计数是如何变化的,以确定其是否在被线程2 真的销毁还是引用计数并没有归零。
基础知识
这里有两个基础概念需要了解:
-
关于
push_back
的行为: 在这个例子中,l1.push_back(std::make_shared<int>(1));
调用涉及到std::shared_ptr
的处理。std::make_shared<int>(1)
创建一个新的shared_ptr
实例。然后,这个新创建的shared_ptr
被传递给push_back
方法。在 C++11 及更高版本中,由于shared_ptr
支持移动语义,push_back
方法会优先使用移动构造函数,如果可行的话。这意味着,在这种情况下,移动构造器将被调用来将shared_ptr
添加到列表中,而不是拷贝构造器。移动构造允许资源(在这个例子中是一个指向int
的指针)从源shared_ptr
(临时创建的)转移到列表中的shared_ptr
,这通常比拷贝构造更高效。 -
关于使用
back
方法得到的shared_ptr
: 当调用l1.back()
时,返回的是列表中最后一个元素的引用。但是,由于列表的元素是std::shared_ptr<int>
类型,所以l1.back()
返回的是对std::shared_ptr<int>
的引用。这意味着,如果你将这个返回值存储在另一个shared_ptr
实例中,将会创建一个新的shared_ptr
实例,该实例与列表中的shared_ptr
共享对同一个int
对象的所有权。这是shared_ptr
的设计目的之一:允许多个shared_ptr
实例共享同一个对象,同时管理其生命周期。在这个过程中,引用计数会适当地增加,以反映共享对象的新所有者。
所以,其实这里的 .back() 和 引用传参 都属于引用 ,线程1 获取到智能指针的过程不应该增加引用计数。
验证1
bool is_l = false;
void DoIt(const std::shared_ptr<int>& i1) {
std::cout << "loop_before: " << i1.use_count() << std::endl;
while (!is_l) {
}
std::cout << "loop: " << i1 << std::endl; // 0x55ccc9b303b0
std::cout << "loop_after: " << i1.use_count() << std::endl;
std::cout << *i1 << std::endl; // 5 , 线程2 修改就是 7
}
/** IS, IS, X integration test */
void MyPointTest() {
// 定义一个 全局链表
std::list<std::shared_ptr<int>> l1;
// 插入数据
l1.push_back(std::make_shared<int>(1));
l1.push_back(std::make_shared<int>(2));
l1.push_back(std::make_shared<int>(3));
l1.push_back(std::make_shared<int>(4));
l1.push_back(std::make_shared<int>(5));
std::cout<<"外面: "<< ((l1.back())).use_count()<< std::endl;
// 定义两个线程
std::thread t_1([&]{
DoIt(l1.back());
});
std::thread t_2([&]{
// 线程2 休眠一会,然后再启动并且执行一些操作
std::this_thread::sleep_for(std::chrono::seconds(3));
for(auto it=l1.begin();it!=l1.end();++it){
if(**it==3){
l1.erase(it);
break;
}
}
for(auto it=l1.begin();it!=l1.end();++it){
if(**it==5){
std::cout << "T2: " << *it << std::endl; // 0x55ccc9b303b0
std::cout << "T2_erase_before: " << (*it).use_count() << std::endl;
l1.erase(it);
std::cout << "T2_erase_after: " << (*it).use_count() << std::endl;
**it = 7;
break;
}
}
is_l = true;
});
t_1.join();
t_2.join();
}
TEST(LockManagerTest, PointTest) { MyPointTest(); } // NOLINT
运行结果
外面: 1 ————说明初始只有 list 中有这个指针
loop_before: 1 ————说明l1.back()是一个引用,传引用参数进入loop不会增加计数
T2: 0x556b687723b0 ———— 地址相同
T2_erase_before: 1 ———— t2 没有erase之前,引用计数依旧是1
T2_erase_after: 1281334311 ———— t2 erase 之后,这个智能指针被销毁
loop: 0x556b687723b0 ———— 地址相同
loop_after: 1281334311 ———— 线程2 销毁,线程1 引用的智能指针也同步销毁、
7 —————— 但是线程2 的修改,线程1 依旧可以读到
验证2
修改代码,让 DoIt 的传参不是一个引用,运行结果为
外面: 1
loop_before: 2 ————此时 传参 就是构造一个拷贝,引用计数增加
T2: 0x55c53c6b83b0
T2_erase_before: 2
T2_erase_after: 1
loop: 0x55c53c6b83b0
loop_after: 1
7
探究3
这里进一步看看单线程下销毁智能指针时引用的指针是否还能使用:
这里设置一个 sp 对 智能指针进行引用,在销毁后查看其是否能访问到这个地址。
for(auto it=l1.begin();it!=l1.end();++it){
if(**it==5){
std::shared_ptr<int> &sp = *it;
std::cout << "T2: " << *it << std::endl; // 0x55ccc9b303b0
std::cout << "T2_erase_before: " << (*it).use_count() << std::endl;
l1.erase(it);
std::cout << "T2_erase_after: " << (*it).use_count() << std::endl;
**it = 7;
std::cout<<"T2_2: "<< *sp << std::endl;
break;
}
}
运行结果
T2_2: 7 -------------所以即使是单线程下,引用计数减少为0,引用依旧可以使用。
结论与思考
无论多线程时还是在单线程,可以通过智能指针的引用获得到同一片地址,并按照智能指针的规则共享的增加或者减少引用计数。但是 当一个唯一的 shared_ptr 被销毁时,其他对这个 智能指针 的引用依旧可以正常访问这个地址。
(有点像内存泄漏的感觉)