智能指针在多线程情况下的问题

本文深入探讨了多线程异步程序中冲突问题的隐蔽性及解决方法,通过实例分析,揭示了智能指针引用计数未加锁导致的内存分配错误,并提出了细致检查和优化策略,旨在帮助开发者识别并解决此类问题。

这两天在测试一个程序的时候发现,一旦压力达到一定程度,程序立即就会崩溃,报的错误几乎都在new的时候没有内存了,一开始以为确实是因为内存分配的问题,后来在程序运行过程中用top观察,发现内存使用很低,因此可以确认不应该是瞬间内存使用完造成的。因此认真看了一下core dump的地方,发现几乎都是在自己写的一个智能指针分配内存那里出的问题。于是仔细思考了一下,发现是因为智能指针的引用计数没有加锁导致的。


一开始以为我所使用的智能指针即使是在异步情况下使用的,但是基本上只能同一时间在一个线程持有,但是事实情况却并非如此


如下代码


void func()

{

  shared_ptr a;

 async_call(a);

}


首先有一个智能指针,接下来,这个智能指针被丢给了异步程序,因此这个时候其实已经有两个线程同时持有这个智能指针了,因为这个函数还未退出,当前线程还拥有临时变量a。一般低压情况下,这两句很快就执行完了,不会出问题,但是高压情况下,这个函数先执行完,还是异步程序先执行完就不一定了(或者说是因为高压情况样本变多了)


结论:多线程异步程序的冲突问题比一般性多线程程序更隐蔽,很难查找,要仔细思考。
智能指针在跨线程场景下存在以下潜在问题: ### 线程安全问题 `std::shared_ptr` 和 `std::weak_ptr` 本身在引用计数的操作上是线程安全的,但对其所指向对象的访问不是线程安全的。如果多个线程同时访问或修改 `std::shared_ptr` 指向的对象,可能会导致数据竞争和未定义行为。 ```cpp #include <iostream> #include <memory> #include <thread> std::shared_ptr<int> sharedData = std::make_shared<int>(0); void increment() { for (int i = 0; i < 100000; ++i) { // 这里的操作不是线程安全的 ++(*sharedData); } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << *sharedData << std::endl; return 0; } ``` 在上述代码中,多个线程同时对 `sharedData` 指向的对象进行自增操作,可能会导致数据竞争。 ### 循环引用问题 在跨线程场景下,循环引用问题可能会更加隐蔽。如果 `std::shared_ptr` 之间形成循环引用,即使所有线程都不再使用这些对象,对象也不会被销毁,从而导致内存泄漏。 ```cpp #include <memory> class B; class A { public: std::shared_ptr<B> b_ptr; ~A() { std::cout << "A destroyed" << std::endl; } }; class B { public: std::shared_ptr<A> a_ptr; ~B() { std::cout << "B destroyed" << std::endl; } }; void createCycle() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->b_ptr = b; b->a_ptr = a; } int main() { createCycle(); return 0; } ``` 在上述代码中,`A` 和 `B` 互相持有对方的 `std::shared_ptr`,导致对象无法被销毁。 ### 性能开销 在高并发场景下,`std::shared_ptr` 的引用计数操作可能会成为性能瓶颈。因为引用计数的增加和减少操作需要进行原子操作,这会带来一定的性能开销。 ```cpp #include <iostream> #include <memory> #include <thread> #include <vector> void incrementSharedPtr(std::shared_ptr<int>& ptr) { for (int i = 0; i < 100000; ++i) { std::shared_ptr<int> localPtr = ptr; } } int main() { std::shared_ptr<int> data = std::make_shared<int>(0); std::vector<std::thread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(incrementSharedPtr, std::ref(data)); } for (auto& thread : threads) { thread.join(); } return 0; } ``` 在上述代码中,多个线程同时对 `std::shared_ptr` 进行引用计数的操作,会带来一定的性能开销。 ### 生命周期管理问题 在跨线程场景下,对象的生命周期管理会变得更加复杂。如果一个线程持有一个 `std::shared_ptr`,而另一个线程可能会提前销毁该对象,可能会导致悬空指针问题。 ```cpp #include <iostream> #include <memory> #include <thread> std::shared_ptr<int> sharedPtr; void destroyObject() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); sharedPtr.reset(); } void accessObject() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); if (sharedPtr) { std::cout << *sharedPtr << std::endl; } else { std::cout << "Object already destroyed" << std::endl; } } int main() { sharedPtr = std::make_shared<int>(42); std::thread t1(destroyObject); std::thread t2(accessObject); t1.join(); t2.join(); return 0; } ``` 在上述代码中,`destroyObject` 线程可能会提前销毁对象,而 `accessObject` 线程可能会在对象销毁后尝试访问该对象,导致悬空指针问题
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值