裸指针与智能指针的线程安全问题

假如想象 提交于 2020-05-03 16:28:39

裸指针线程安全问题

使用普通裸指针造成的问题

#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(互斥锁))

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!