文章目录
裸指针线程安全问题
使用普通裸指针造成的问题
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void funA()
{
cout << "A的一个非常好用的一个方法" << endl;
}
};
void hander01(A *p)
{
std::this_thread::sleep_for(std::chrono::seconds(2));
p->funA();
}
int main()
{
A *p = new A();
thread t1(hander01,p);
delete p;
t1.join();
}
运行结果:
A()
~A()
A的一个非常好用的一个方法
A在析构完成之后还可以调用A的方法,这个操作是极其不安全的一个操作的,所以我们可以使用强弱智能指针来使得操作变得安全起来。
shared_ptr 和 weak_ptr的解决问题
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void funA()
{
cout << "A的一个非常好用的一个方法" << endl;
}
};
void handler01(weak_ptr<A> q)
{
//需要检测对象A是否存活
std::this_thread::sleep_for(std::chrono::seconds(2));
shared_ptr<A> ptmp = q.lock();
if (ptmp != nullptr)
{
ptmp->funA();
}
else
{
cout << "A对象已经析构" << endl;
}
}
int main()
{
{
shared_ptr<A> p(new A());
thread t1(handler01, weak_ptr<A>(p));
t1.detach();
}
std::this_thread::sleep_for(std::chrono::seconds(10));
}
运行结果:
A()
~A()
A对象已经析构
shared_ptr的线程安全问题
智能指针shared_ptr本身(底层实现原理是引用计数)是线程安全的
智能指针的引用计数在手段上使用了atomic原子操作,只要shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全的,所有智能指针在多线程下引用计数也是安全的
智能指针指向的对象的线程安全问题,智能指针没有做任何保障
对于智能指针shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象,当智能指针发生拷贝的时候,标准库的实现是先拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子操作,隐患就出现在这里。
比如A线程在拷贝智能指针,因为不是原子操作,恰好进行线程切换,导致没有及时调用引用计数,另一个线程B把上一线程被拷贝的指针指向了新的智能指针,此操作把拷贝智能指针,再拷贝引用计数都做完了,那么之前的引用计数减为了0,指针是否.此时切换到线程A,开始调用引用计数,调用的就是已经切换后B指针的引用计数,但A指针的指向还是最初指针的悬挂指针。
如果还不明白,引用一下陈硕老师的例子:
地址点击打开链接
多线程编程中的三个核心概念
(1)原子性的举例
这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。
关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。
(2)可见性的举例
可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。可见性问题是好多人忽略或者理解错误的一点。
CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程读取共享变量时,都会将该变量加载进其对应CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非第一个线程更新后的数据。
这一点是操作系统或者说是硬件层面的机制,所以很多应用开发人员经常会忽略。
(3)顺序性举例
顺序性指的是,程序执行的顺序按照代码的先后顺序执行。处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。讲到这里,有人要着急了——什么,CPU不按照我的代码顺序执行代码,那怎么保证得到我们想要的效果呢?实际上,大家大可放心,CPU虽然并不保证完全按照代码顺序执行,但它会保证程序最终的执行结果和代码顺序执行时的结果一致。
总结
如果你能保证不会有多个线程同时修改或替换指针指向的对象,不用加锁是完全没有问题的。
如果希望在多个线程使用同一个对象的智能指针,可以让每个线程使用这个指针的不同副本或者使用锁保护这个指针。
解决办法-加入锁机制
使用互斥锁对多线程读写同一个shared_ptr进行加锁操作(多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁))
来源:oschina
链接:https://my.oschina.net/u/4287715/blog/4263097