Linux线程安全,死锁,生产消费模型,线程池

目录

1.可重入和线程安全

2.死锁

死锁四个必要条件:

 避免死锁

3. Linux线程同步

线程同步

 生产消费模型的概念理解(321原则)

生产消费模型都有哪些好处。

串行、并发、并行

 条件变量

 4.信号量

5.线程池

 线程池模型

6.单例模式

饿汉与懒汉两种单例模式

7.智能指针和线程安全

STL中的容器是否是线程安全的?

智能指针是否是线程安全的?

8.其他锁

自旋锁



人的一生为什么要坚持?因为最痛苦的事,不是失败,是我本可以。 

1.可重入和线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。 常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。 一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。
可重入与线程安全联系
1.可重入函数是线程安全函数的一种
2.线程安全不一定是可重入的,而可重入函数则一定是线程安全的
3.函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

2.死锁

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

死锁四个必要条件:

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

 避免死锁

破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配

银行家算法是避免出现死锁的一种算法(并非预防的方法) 

行家算法的思想是为了避免出现“环路等待”条件

 鸵鸟策略 对可能出现的问题采取无视态度,前提是出现概率很低

预防策略 破坏死锁产生的必要条件

避免策略 银行家算法,分配资源前进行风险判断,避免风险的发生

检测与解除死锁 分配资源时不采取措施,但是必须提供死锁的检测与解除手段

3. Linux线程同步

线程同步

我们可以举一个例子来理解条件变量是如何实现线程同步的。


假设现在学校开了一间学霸vip自习室,学校规定这间自习室一次只能进去一个人上自习,自习室门口挂着一把钥匙,谁来的早先拿到这把钥匙,就可以打开门进入自习室学习,并且进入自习室之后,把门一反锁,其他人谁都不能进来。然后你第二天准备去学习了,卷的不行,直接凌晨三点就跑过来,拿着钥匙进入自习室上自习了,然后卷了3小时之后,你想出来上个厕所,一打开门发现外面站的一堆人,都在叽叽喳喳的讨论谁先来的,怎么来的这么早?这么卷?然后你怕自己等会儿把钥匙放到墙上之后,上完厕所回来之后有人拿着钥匙进入了自习室,你就又卷不了了,所以你把钥匙揣兜里,拿着钥匙去上厕所了,其他人当然进入不了自习室,因为你拿着钥匙去上厕所了。等你回来的时候,你又打开门,又来里面上了3小时自习,你感觉自己饿的不行了,在不吃饭就饿死在里面了,所以你打开门,准备出去吃饭了,然后突然你自己感觉负罪感直接拉满,我凌晨3点好不容易抢到自习室,现在离开是不太亏了,所以你又打开自习室回去上自习去了,别人当然竞争不过你呀!因为钥匙一直都在你兜里,你出来之后把钥匙放到墙上,你发现有点负罪感,你又拿起来钥匙回去上自习,因为你离钥匙最近,所以你的竞争能力最强。结果你来自习室上了1分钟自习又出来了,然后又负罪的不行,又回去了,周而复始的这么干,结果别人连自习室长啥样都没见到。
像这样由于长时间无法得到锁的线程,没办法进入临界区访问临界资源,我们称这样的线程处于饥饿状态!

 

所以学校推出了新政策,所有刚刚从自习室出来的人,都必须回到队列的尾部重新排队等待进入自习室,这样的话,其他人也就可以拿到钥匙进入自习室了。


所以,在保证数据安全的前提下,让线程能够按照某种特定的顺序来访问临界资源,从而有效避免其他线程的饥饿问题,这就叫做线程同步!

引入同步:主要是为了解决访问临界资源合理性问题的

 生产消费模型的概念理解(321原则)

实际生活中,我们作为消费者,一般都会去超市这样的地方去购买产品,而不是去生产者那里购买产品,因为供货商一般不零售产品,他们都会统一将大量的商品供货到超市,然后我们消费者从超市这样的交易场所中购买产品。
而当我们在购买产品的时候,生产者在做什么呢?生产者可能正在生产商品呢,或者正在放假呢,也可能正在干着别的事情,所以生产和消费的过程互相并不怎么影响,这就实现了生产者和消费者之间的解耦。
而超市充当着一个什么样的角色呢?比如当放假期间,消费爆棚的季节中,来超市购买东西的人就会非常的多,所以就容易出现供不应求的情况,但超市一般也会有对策,因为超市的仓库中都会预先屯一批货,所以在消费爆棚的时间段内,超市也不用担心没有货卖的情况。而当工作期间,大家由于忙着通过劳动来换取报酬,可能来消费的人就会比较少,商品流量也会比较低,那此时供货商如果还是给超市供大量的货呢?虽然超市可能最近确实卖不出去东西,但是超市还是可以把供货商的商品先存储到仓库中,以备在消费爆棚的季节时,能够应对大量消费的场景。所以超市其实就是充当一个缓冲区的角色,在计算机中充当的就是数据缓冲区的角色

从生产消费模型中可以提取出来一个321原则。即为3种关系,两个角色,1个交易场所。对应的其实是消费线程和消费线程的关系,消费线程和生产线程的关系,生产线程和生产线程的关系,交易场所就是阻塞队列blockqueue。而实现线程同步就需要一个条件变量,比如生产者生产完之后,超市给消费者打个电话,让消费者过来消费,消费完之后,超市在给生产者打个电话,让生产者来生产,这样就不会存在由于某一个线程竞争能力过强,一直生产或一直消费的情况产生,从而导致其他线程饥饿的问题。
 

生产消费模型都有哪些好处。

他实现了生产和消费的解耦,使他们之间并不互相影响。
支持生产和消费一段时间的忙闲不均的问题。因为缓冲区可以预留一部分数据,进行数据的缓冲。

由于生产和消费的互斥与同步关系,提升了生产消费模型的效率

生产消费模型并不高效在放任务到阻塞队列和从阻塞队列拿任务,而是真正高效在,某一个线程拿或放任务到blockqueue的时候,并不会影响其他线程并发或并行的获取任务和执行任务。

串行、并发、并行

 条件变量

初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attrNULL

销毁 

int pthread_cond_destroy(pthread_cond_t *cond)

等待条件满足 

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待 

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

 4.信号量

信号量本质:


是一个计数器,访问临界资源的时候,必须先申请信号量资源(sem--,预订资源,P)使用完毕信号量资源(sem++,释放资源,V)

每一个线程想要访问临界资源中的小块儿资源时,都需要先申请信号量,申请信号量成功后,才可以访问小块儿资源。

信号量的申请和释放并不是简单的++或- -,他的申请和释放操作应该是原子的,信号量- -实际对应的是P操作,信号量++对应的是V操作,所以信号量的核心操作是PV操作,或者叫做PV原语。 

初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值

哪几种方式可用来实现线程间通知和唤醒:
信号量和条件变量通过提供的使线程等待和唤醒功能被用于实现线程间的同步,

信号量既可以实现同步还可以实现互斥
条件变量需要搭配互斥锁使用,信号量不需要

5.线程池

线程池是一种用于管理和复用线程的机制,它的作用是通过提前创建一组线程,并将任务分配给这些线程来提高程序的性能和效率。

应用场景

 线程池模型

线程池模型实际就是生产消费模型,我们会在线程池中预先准备好并创建出一批线程,然后上层将对应的任务push到任务队列中,休眠的线程如果检测到任务队列中有任务,那就直接被操作系统唤醒,然后去消费并处理任务,唤醒一个线程的代价是要比创建一个线程的代价小很多的。


而实际下面线程池的模型不就是我们一直学的生产消费模型吗?那些任务线程就是生产者,任务队列就是交易场所,处理线程就是消费者。所以听起来高大上的线程池本质还是没有脱离开我们一直所学的生产消费模型,所以实现线程池顶多在技巧和细节上比以前要求高了一些,但在原理上和生产消费模型并无区别。

6.单例模式

IT 行业这么火 , 涌入的人很多 . 俗话说林子大了啥鸟都有 . 大佬和菜鸡们两极分化的越来越严重 . 为了让菜鸡们不太拖 大佬的后腿, 于是大佬们针对一些经典的常见的场景 , 给定了一些对应的解决方案 , 这个就是 设计模式
某些类, 只应该具有一个对象(实例), 就称之为单例.
例如一个男人只能有一个媳妇 .
在很多服务器开发场景中 , 经常需要让服务器加载很多的数据 ( 上百 G) 到内存中 . 此时往往要用一个单例的类来管理 这些数据.

饿汉与懒汉两种单例模式


单例模式就是只能有一个实例化对象的类,这个类我们可以称为单例。而实现单例的方式通常有两种,分别就是懒汉实现方式和饿汉实现方式


举一个形象化的例子,懒汉就是吃完饭,先把碗放下,然后等到下一顿饭的时候再去洗碗,这就是懒汉方式。而饿汉就是吃完饭,立马把碗洗了,下一顿吃饭的时候,就不用再去洗碗了,而是直接拿起碗来吃饭,这就是饿汉实现方式。


虽然生活中懒汉还是不太好的,因为生活比较乱和邋遢。但在计算机中,懒汉方式还是不错的,懒汉最核心的思想就是延迟加载,这样的方式能够优化服务器的速度,即为你需要的时候我再给你分配,你现在还用不着,那我就先不给你分配,这就是延迟加载。

2.
像饿汉这样的方式,实际是非常常见的,因为延时加载这样的管理思想对于软硬件资源管理者OS而言,实际是很优的一种管理手段。就比如平常的malloc和new,操作系统底层在开辟空间的时候,实际并不是以饿汉的方式来给我们开辟的,而是以懒汉的方式来给我们开辟的。等到程序真正访问和使用要申请的内存空间时,会触发缺页中断,操作系统此时知晓之后才会真正给我们在物理内存上开辟相应申请大小的空间,重新构建虚拟和物理的映射关系,返回对应的虚拟地址。

7.智能指针和线程安全

STL中的容器是否是线程安全的?

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

智能指针是否是线程安全的?

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

8.其他锁

我们重点谈一下自旋锁,其他了解一下就可以

自旋锁

以前我们学到的互斥锁,信号量这些,一旦申请失败,线程就会被阻塞挂起,我们称这样的锁为挂起等待锁,因为线程需要去PCB维护的等待队列中进行wait,直到锁被释放。
而自旋锁如果申请失败,线程并不会挂起等待,它会选择自旋,循环检查锁的状态是否被释放,这种方式可以减少线程上下文切换时所带来的性能开销,但同时也会带来CPU资源的浪费,因为你这个线程一直霸占CPU不断轮询锁的状态,CPU就无法调度其他线程了。
所以使用挂起等待锁和自旋锁,主要依据就是线程需要等待的时间长短,或者说成是申请到锁的线程在临界区中待的时间长短,如果时间较长,那么选择挂起等待锁来进行加锁保护临界资源的方案就比较合适,如果时间较短,那么选择自旋锁不断轮询锁的状态,用自旋锁来进行临界资源的保护方案就比较合适。

 举个例子说明一下,我想等张三一起去吃饭,张三说还有1小时就下来,我不能在宿舍楼下等一小时啊,我可以去学校外面网吧上会网,他好了之后我再回来。   这相当于互斥锁

张三说还有1分钟就下来,那我等他,1分钟他还没下来,我就打电话问他,他说快了快了,结果还没下来,我接着打电话。这种情况是自旋锁

自旋锁的本质是通过不断检测锁状态,来进行资源是否就绪的方案


 

9.一些题

1.无锁化编程有哪些常见方法?

A.针对计数器,可以使用原子加

B.只有一个生产者和一个消费者,那么就可以做到免锁访问环形缓冲区(Ring Buffer)

C.RCU(Read-Copy-Update),新旧副本切换机制,对于旧副本可以采用延迟释放的做法

D.CAS(Compare-and-Swap),如无锁栈,无锁队列等待

2.java/C/C++中volatile关键字可以保证并发编程中的

可见性,有序性

3.下列操作中,需要执行加锁的操作是() 

A.x++;

B.x=y;

C.++x;

D.x=1;

————————————————
本文参照了下面大佬的文章
原文链接:https://blog.csdn.net/erridjsis/article/details/130547353

  • 17
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值