首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >线程安全及其他理论

线程安全及其他理论

作者头像
用户11029129
发布2024-10-27 07:42:04
发布2024-10-27 07:42:04
20800
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

🚀死锁概念

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态

  如果现在有两个线程thread1和thread2,整个体系之中存在两把锁,mutex1和mutex2,现在出现这这样一种情况:

  thread1已经持有锁1,thread2已经持有锁2,而此时thread1需要mutex2锁才能继续执行,那么thread1就等待mutex2释放,而thread2这时也需要mutex1才能继续执行,所以thread2也需要等待thread1释放mutex1,这样两个线程相互等待对方释放锁才能继续运行后续代码。

其实很简单,张三来到一个小吃摊前面,跟老板说,xxx好吃吗?我先来尝尝,好吃我就买。老板这时就急眼了,小本生意,不买就别吃。张三又说,我不吃怎么知道好不好吃,好吃我才买。总之张三跟老板谁也不让。这就是一种死锁,虽然现实当中这种事很荒唐,但是在计算机当中这种问题是闲荡重要的

  这样的情形我们称为死锁,也是最常见的死锁情况,死锁会导致占用公共资源不释放,甚至可能会造成饥饿问题,所以程序员需要避免死锁的情况。

死锁产生的四个必要条件

  • 互斥条件一个资源每次只能被一个执行流使用
  • 请求与保持条件一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件: 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系

✈️解决死锁方法

   而我们想要避免掉死锁,只需要破坏死锁产生条件的任意一条或者多条,就可以避免死锁。

破坏互斥条件,一句话:能不加锁就不加锁,有些代码不需要加锁我们就尽量不加锁,避免死锁问题最有效的方式就是不加锁。不过需要限制资源访问以及并发问题时还是需要加锁的,这个时候我们只能破坏另外的三个条件了。

破坏请求与保持条件,这个条件简单来说就是,我不仅要吃自己的食物,我还要吃你的食物。即申请一个锁之后申请另外一个锁,比如一个线程需要申请锁1和锁2,此时已经拥有锁1,但还需要申请锁2,这个时候很可能会发生死锁的情况(别的线程刚好持有锁2而恰好又需要锁1, 导致互相等待对方释放)。所以我们可以对锁2申请的时候进行判断,如果申请失败则释放锁1,申请成功继续执行。

破坏不剥夺条件:这个条件的破坏情况比较复杂,如果一些特殊的场景确实需要几把锁,我们实际上不能随意剥夺。当然,我们可以想办法来剥夺,我们可以像进程那样,给线程设置优先级,优先级高的可以剥夺优先级低的线程的锁。

破坏循环等待条件:建议如果线程需要申请多把锁,每个线程申请的锁的顺序一致,尽量避免,这样就可以避免环路等待,从而避免死锁。


🚀C++中STL、智能指针是否线程安全
✈️STL容器线程安全

  STL容器不能保证线程安全。原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全

✈️ 智能指针线程安全

  对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。


🚀其他常见各种锁
  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁?

  我们前面学习的不论是互斥锁还是二元信号量都属于悲观锁的范畴,担心数据被其线程修改所以提前加锁。


🚀单例模式线程安全

  如果你学过设计模式,那么一定知道有一种常见的单例模式叫做懒汉模式,不知道也没关系,我来简单介绍一下:

  什么是单例模式?顾名思义,单例模式实际上就是在整个进程当中,这个类的对象有且只有一个,当然,仅限于当前进程。而单例模式分为饿汉模式与懒汉模式。

  懒汉模式相当于一个人想要吃饭了再去做饭。以下的代码就是一个简单的懒汉式单例模式:

代码语言:javascript
代码运行次数:0
运行
复制
template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() 
	{
		if (inst == NULL) 
		{
			inst = new T();
		}
	
		return inst;
	}
};

  当代码中第一次调用GetInstance时,会创建单例对象,而后每次调用的单例对象都是最初创建的单例对象。这样就能保证创建出来的一定是单例。但是引入了线程,如果多个线程恰好并发初次执行GetInstance()接口,这样就会创建出多个类对象,也就不能称之为单例了。

  那么创建单例的过程就变为了临界资源,所以在懒汉模式下,我们需要给GetInstance()内的临界区加锁保证同一时刻只有一个线程在创建单例:

代码语言:javascript
代码运行次数:0
运行
复制
template <typename T>
class Singleton {
	static T* inst;
public:
	static T* GetInstance() 
	{
		if(nullptr == inst)// 如果第二次及以后获取单例我们不再加锁,直接返回即可
		{
			lock();// 第一次获取单例需要加锁
			if (inst == NULL) 
			{
				inst = new T();
			}
			unlock();
		}
		
	
		return inst;
	}
};

✈️自旋锁

  我们从来没有讨论过线程在临界区内执行时间长短的问题:

有一天阿熊11点钟去小美楼下给小美发消息一起吃饭,小美一看手机才11点,而且目前自己还有事情,所以小美就跟阿熊说:“这才几点钟,我还要好一会才去吃饭,你要是急你先自己去吧”。阿熊收到消息后,毅然决然的给小美发消息:“那我先去网吧打会游戏,你好了再叫我”。于是到了1点钟,小美就叫阿熊一起去吃饭了第二天,阿熊这次12点来找小美共进午餐,这个时候小美刚好饿了,眼下也没事情,于是小美给阿熊回电话:“我等下就下来”。于是阿熊就一直在楼下等待,果不其然,没几分钟小美就下来了

  这是一个很平常的故事,映射到线程,其中阿熊和小美是两个线程,阿熊看小美还要很久才会下来,于是就去网吧打游戏去了。实际上这种行为就是线程在临界区内执行时间比较长,所以其他线程会阻塞挂起,相当于阿熊等小美。

  而第二次阿熊叫小美去吃饭,小美说马上就下楼来一起去吃饭,于是阿熊就一直在宿舍楼下等小美下楼共进午餐,这种行为实际上就是其他线程在 自旋,那么不难看出,自旋实际上就是不断的对资源发出请求,当资源空闲就可以快速占有资源。

  我们之前一直用的互斥锁都属于第一种情况的锁,而对于一些在临界资源访问时间比较短的线程来说,可以采用自旋锁来对线程进行加锁控制,而在Linux的pthread线程库当中,给我们提供了自旋锁的一些接口:

  这种接口大家应该都不陌生,与互斥锁的使用除了名称不同几乎没什么差别。可以直接平替互斥锁加锁。而我们所说的自旋的过程在函数内部就已经帮助我们自旋。


🚀读者写者问题

  线程中有两个经典的应用场景,一个是生产消费模型,另一个就是读者写者模型。

  读者写者模型应用场景很多,以下几种仅为举例:在csdn发文章,杂志,出黑板报等等都是读者写者模型。读者写者模型常见情况是——读者众多,写者较少

  读者写者模型也符合“321”原则,即:

  • 3种关系写者与写者,读者与写者,读者与读者

写者和写者之间属于竞争,所以是互斥关系。读者与写者之间的关系一定有互斥,比如当一个人在写工作报告,但是没写完这时候被接收人看到直接把没写完的数据汇报,就会造成数据不一致的问题。而同时它们又要保持同步关系,因为工作报告写完需要提交给接收人,接收人需要读取工作报告获取进度信息而 读者和读者之间,实际上 没有关系,读者写者模型并不是生产消费者模型,因为消费者之间会存在消费行为,会消耗资源。而读者写者模型读者之间仅仅需要读取拷贝,并不需要对资源进行更改,所以读者与读者之间没有关系

  • 2个角色读者,写者
  • 1个场所一个读写场所

  依照上述的321原则,再根据之前的生产消费者模型我们不难写出伪代码:

  因为读写者模型有两个角色,并且工作不同,所以需要定义两把锁将其加锁处理,同时使用计数器记录读者是否正在读取。

  读者在读取数据的时候需要加锁,如果是第一个读者,会分为两种情况,一种是写者正在写,而写者在写时,需要跟写者保持互斥关系,所以在访问数据时,也需要申请wlock。在申请成功后,将读者read_count+1,这样其他读者线程就进不来了。随后在进行常规的读取拷贝操作。、

  读取完成之后,需要将reader_count自减,把读者状态置为0,让其他读者线程可以进行申请锁,申请数据资源。而在正常的开发中,pthread线程库里给我们提供了 读写锁的接口

代码语言:javascript
代码运行次数:0
运行
复制
#include <pthread.h>

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);// 销毁读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restric attr);// 初始化读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);// 读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);// 写者加锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);// 解锁

✈️读者优先与写者优先

  读者优先与写者优先就是字面含义,是读者先读还是写者先写,是一种同步顺序问题。通常情况下,一般是都是读者优先,不过 读者优先有可能导致写者饥饿问题,因为读者读取时需要持有写者的锁,如果读者线程持续不断地抢占写者的锁,就会造成写者饥饿问题。

  这个问题对于读写者模型并不是很严重,因为按照正常情况来说,一个作者写好作品,大部分时间就是读者在读,少许时间是写者在写。所以可以将其看作读写者模型的一个特点。

  如果实在对这个问题感到厌恶,可以采用写者优先的方式来写代码,不过这样就必然会使得写者代码变得更加复杂,比如需要添加标记为,记录读者是否来了,添加等待队列等等。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-10-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ✈️解决死锁方法
  • 🚀C++中STL、智能指针是否线程安全
    • ✈️STL容器线程安全
    • ✈️ 智能指针线程安全
  • 🚀其他常见各种锁
  • 🚀单例模式线程安全
    • ✈️自旋锁
  • 🚀读者写者问题
    • ✈️读者优先与写者优先
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档