shared_ptr 和线程安全的问题
文章目录
小黑:小辉,今天我们来看一下shared_ptr
和线程安全方面的问题吧
小辉:小黑,线程安全还能和shared_ptr智能指针
扯上关系吗?
小黑:当然有关系了,好好看一下今天的博客吧.
引入线程之后shared_ptr 引发的线程安全问题
- 引用实例
#include<iostream>
#include<thread>
#include<mutex>//互斥锁
using namespace std;
struct Date
{
int _year = 1997;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
template <class T>
class SharedPtr{
public:
//构造函数在第一次申请资源后调用
SharedPtr(T* ptr)
:_ptr(ptr)
, _useCount(new int(1))//第一次申请资源的时候指向这片资源的指针个数为1
{}
//拷贝构造的使用
SharedPtr(const SharedPtr<T> & sp)
:_ptr(sp._ptr)
, _useCount(sp._useCount)
{
//当前指向资源的引用计数+1;
++(*_useCount);
}
//赋值运算符重载
SharedPtr<T> &operator=(const SharedPtr<T> & sp)
{
//对于两个智能指针管理同一片空间的情况,不需要赋值
if (_ptr != sp._ptr){//检查是否为自己给自己赋值
if (--(*_useCount) == 0)
//检查是否是最后的一个指针指向这片空间
{
delete _ptr;
delete _useCount;
//若是最后一个指针则将这片空间释放并且将计数器指针也释放掉
}
_ptr = sp._ptr;
_useCount = sp._useCount;
++(*_useCount);
}
return *this;
}
//析构函数的使用
~SharedPtr()
{
if (--(*_useCount) == 0){
delete _ptr;
delete _useCount;
_ptr = nullptr;
_useCount = nullptr;
}
}
T& operator*(){
return *_ptr;
}
T* operator ->(){
return _ptr;
}
int useCount(){
return *_useCount;
}
private:
T* _ptr;
int* _useCount;
};
void Test(const SharedPtr<Date> &sp,int n){
for (int i = 0; i < n; i++)
SharedPtr<Date> spcopy(sp);
}
int main(){
//Date* dt = new Date;
SharedPtr<Date> sp(new Date);
int n = 100000;
thread t1(Test, sp, n);
thread t2(Test, sp, n);
t1.join();
t2.join();
cout << sp.useCount() << endl;
return 0;
}
- 运行结果
小黑:从多次的运行录频看的出每次的运
行结果都是不同的,这里就体现出了
线程的不安全问题
加入互斥锁之后的shared_ptr
- 使用实例
//shared_ptr与线程安全的问题
//加入互斥锁来保证线程的安全
#include<iostream>
#include<thread>
#include<mutex>//互斥锁
using namespace std;
mutex mtx;
//定义一个全局的锁
struct Date
{
int _year = 1;
int _month = 8;
int _day = 1;
~Date(){
cout << "~Date()" << endl;
}
};
template <class T>
class SharedPtr{
public:
//构造函数在第一次申请资源后调用
SharedPtr(T* ptr)
:_ptr(ptr)
, _useCount(new int(1))//第一次申请资源的时候指向这片资源的指针个数为1
, _mtx(new mutex)//空间与创建的锁对应起来
{}
//拷贝构造的使用
SharedPtr(const SharedPtr<T> & sp)
:_ptr(sp._ptr)
, _useCount(sp._useCount)
, _mtx(sp._mtx)
{
//当前指向资源的引用计数+1;
addRef();
}
//赋值运算符重载
SharedPtr<T> &operator=(const SharedPtr<T> & sp)
{
//对于两个智能指针管理同一片空间的情况,不需要赋值
if (_ptr != sp._ptr){//检查是否为自己给自己赋值
if (subRef() == 0)
//检查是否是最后的一个指针指向这片空间
{
delete _ptr;
delete _useCount;
//若是最后一个指针则将这片空间释放并且将计数器指针也释放掉
delete _mtx;
}
_ptr = sp._ptr;
_useCount = sp._useCount;
_mtx = sp._mtx;
addRef();
}
return *this;
}
//对引用计数进行自加操作的函数
int addRef(){
_mtx->lock();
++(*_useCount);
_mtx->unlock();
return *_useCount;
}
//对引用计数进行自减操作的函数
int subRef(){
_mtx->lock();
--(*_useCount);
_mtx->unlock();
return *_useCount;
}
//析构函数的使用
~SharedPtr()
{
if (subRef() == 0){
//检查是否是最后一个指针指向这片空间
//以确保释放的空间有效
delete _ptr;
delete _useCount;
delete _mtx;
_mtx = nullptr;
_ptr = nullptr;
_useCount = nullptr;
}
}
T& operator*(){
return *_ptr;
}
T* operator ->(){
return _ptr;
}
int useCount(){
return *_useCount;
}
private:
T* _ptr;
int* _useCount;
mutex* _mtx;
};
void Test( SharedPtr<Date> &sp, int n){
for (int i = 0; i < n; i++){
//智能指针本身是线程安全的,但是管理的资源不一定线程安全
//需要资源的使用者保证线程安全
SharedPtr<Date> spcopy(sp);
mtx.lock();
sp->_year++;
//对年份进行++操作,验证线程锁的可靠性
mtx.unlock();
}
}
int main(){
//Date* dt = new Date;
SharedPtr<Date> sp(new Date);
int n = 100000;
thread t1(Test, sp, n);
thread t2(Test, sp, n);
t1.join();
t2.join();
cout << sp.useCount() << endl;
cout << sp->_year << endl;
return 0;
}
- 运行结果
小黑:由程序运行结果看出,该函数的功能
实现了多份资源逐一进行使用的功能
,达到了保护线程安全的目的
小辉:但是,大家要记住奥,虽然加锁能
保证线程的安全,同时消耗也很大
- 1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的。
- 2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
【总结】
- 该程序中体现出了线程锁使用的必要性
- 该程序中声明了一个全局的线程锁,可以保证整个类中加锁解锁的便利性,减少了不必要的麻烦。
- 同时使用构造函数和析构函数实现了保证了引用计数的安全性提高了线程安全的可靠性,通过线程锁体现出了RAII思想。
std::shared_ptr的循环引用
- 使用实例
//shared_ptr 的循环引用
#include<iostream>
#include<memory>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
小黑:该函数中体现了shared_ptr的循环引用
在两者之间建立了直接联系,如下图所示
小辉:原来是将他们"首尾相连"了啊
,这样的话他们对彼此的依赖
就变大了
- 运行结果
小辉:小黑,为啥两个结点在生命周期结束时
没有调用析构函数啊?
小黑:小辉,你看看下面的分析你就知道了
【循环引用分析】:
- 1.node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- 2.node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- 3.node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 4.也就是说_next析构了,node2就释放了。
- 5.也就是说_prev析构了,node1就释放了。
- 6.但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
【问题的出现】
- 如何才能实现两者的析构呢?
weak_ptr 的引入
小黑:他可是专门为解决shared_ptr循环引用
的析构问题而引入的
- 使用实例
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数。
#include<iostream>
#include<memory>
using namespace std;
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
//换成weak_ptr即可调用ListNode 的析构函数
};
int main(){
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
- 运行结果
小辉:通过运行结果大家可以看到使用了
weak_ptr后两个结点的引用计数并未
增加,而且还调用了析构函数
线程的死锁问题
小辉:小黑,死锁是怎么形成的呢?
小黑:当两个线程执行同一指令时,第一个
线程拿到执行逻辑后先加上了锁?,
但是因为某种原因导致异常出现,
没有解锁.那么另一个线程就被一直
挡在线程锁之外,无法拿到资源继续
执行代码逻辑,这种现象就叫死锁
- 使用实例
//异常安全的问题线程的死锁
#include<iostream>
#include<mutex>
#include<vector>
#include<thread>
using namespace std;
mutex mtx;
void fun(){
try{
mtx.lock();
vector<int> v;
v.at(10) = 10; //异常,跳转,导致第一个线程未解锁,造成死锁
mtx.unlock();
}
catch (exception & e){
cout << e.what() << endl;
}
cout << "exception dealad" << endl;
}
int main(){
thread t1(fun);
thread t2(fun);
t1.join();
t2.join();
return 0;
}
【执行结果】
- 程序奔溃
守卫锁
小辉:哇奥,给人一种皇宫守卫的
感觉.
小黑:这种锁可以避免线程死锁的出现奥
,并且还采用了RAII的思想
#include<iostream>
#include<mutex>
#include<vector>
#include<thread>
using namespace std;
mutex mtx;
//守卫锁
template <class MTX>
class LockGurad{
public:
LockGurad(MTX& mtx)
:_mtx(mtx)
{
_mtx.lock();
//构造的时候加锁
}
~LockGurad(){
_mtx.unlock();
//析构的时候解锁
}
LockGurad(const LockGurad<mutex> &) = delete;
//防止拷贝
private:
MTX& _mtx;
};
void fun(){
try{
LockGurad<mutex> lg(mtx);
int i;
cin >> i;
if (i > 100)
return ;
}
catch (exception & e){
cout << e.what() << endl;
}
cout << "exception dealad" << endl;
}
int main(){
thread t1(fun);
thread t2(fun);
t1.join();
t2.join();
return 0;
}
- 运行结果
【守卫锁总结】
- 在创建的对象时会自动调用构造函数加锁,在对象生命周期结束后会自动调用析构函数进行解锁。
- 既解决了线程死锁的问题,还体现了RAII的思想,是一种比较可靠的实现方式
仿函数的删除器
【问题】
- 如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题
小黑:我们可以使用仿函数的删除器
来完成资源的清理
- 使用实例
#include<iostream>
#include<memory>
using namespace std;
struct ListNode
{
int _data;
/*shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;*/
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
//换成weak_ptr即可调用ListNode 的析构函数
};
struct Date{
int _year = 1997;
int _month = 1;
int _day = 2;
~Date(){
cout << "~Date()" << endl;
}
};
template<class T>
struct Free{
void operator()(T* ptr){
cout << "free:"<<ptr << endl;
free(ptr);
}
};
template <class T>
struct DeleteArray{
void operator()(T* ptr){
cout << "delete[]:" << ptr << endl;
delete[] ptr;
ptr = nullptr;
}
};
int main(){
Free<Date> FREE;
DeleteArray<Date> DelArray;
shared_ptr<Date> sp(new Date);
shared_ptr<Date> sp1((Date*)malloc(sizeof(Date)), FREE);
shared_ptr<Date> sp2(new Date[10], DelArray);
return 0;
}
- 运行结果
小辉:由其结果可以看到new出的对象
之,malloc出的对象都可以使用
删除器来完成资源的回收
小黑,小辉:一起来学习C++吧,
智能指针也会引起线程安全的问题呢.
期待下期再见奥.