目录
1.可重入和线程安全
2.死锁
死锁四个必要条件:
避免死锁
3. Linux线程同步
线程同步
生产消费模型的概念理解(321原则)
生产消费模型都有哪些好处。
串行、并发、并行
条件变量
4.信号量
5.线程池
人的一生为什么要坚持?因为最痛苦的事,不是失败,是我本可以。
1.可重入和线程安全
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:要初始化的条件变量attr:NULL
销毁
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.单例模式
饿汉与懒汉两种单例模式
单例模式就是只能有一个实例化对象的类,这个类我们可以称为单例。而实现单例的方式通常有两种,分别就是懒汉实现方式和饿汉实现方式。
举一个形象化的例子,懒汉就是吃完饭,先把碗放下,然后等到下一顿饭的时候再去洗碗,这就是懒汉方式。而饿汉就是吃完饭,立马把碗洗了,下一顿吃饭的时候,就不用再去洗碗了,而是直接拿起碗来吃饭,这就是饿汉实现方式。
虽然生活中懒汉还是不太好的,因为生活比较乱和邋遢。但在计算机中,懒汉方式还是不错的,懒汉最核心的思想就是延迟加载,这样的方式能够优化服务器的速度,即为你需要的时候我再给你分配,你现在还用不着,那我就先不给你分配,这就是延迟加载。
2.
像饿汉这样的方式,实际是非常常见的,因为延时加载这样的管理思想对于软硬件资源管理者OS而言,实际是很优的一种管理手段。就比如平常的malloc和new,操作系统底层在开辟空间的时候,实际并不是以饿汉的方式来给我们开辟的,而是以懒汉的方式来给我们开辟的。等到程序真正访问和使用要申请的内存空间时,会触发缺页中断,操作系统此时知晓之后才会真正给我们在物理内存上开辟相应申请大小的空间,重新构建虚拟和物理的映射关系,返回对应的虚拟地址。
本文参照了下面大佬的文章
原文链接:https://blog.csdn.net/erridjsis/article/details/130547353