Linux相关概念和易错知识点(31)(生产者-消费者模型、认识信号量、线程安全)

目录

1.生产者-消费者模型

(1)认识生产者-消费者模型

(2)3种关系、2种角色、1个交易场所

①3种关系

②2种角色、1个交易场所

③“321”原则

(3)基于BlockingQueue(阻塞队列)的生产者-消费者模型

(4)基于环形队列的生产者-消费者模型

(5)模型的效率

2.认识信号量

3.线程安全

(1)死锁

①什么是死锁

②死锁的四个必要条件

(2)线程安全和重入

①线程安全

②两种重入的情况

③重入和线程安全的区别


1.生产者-消费者模型

(1)认识生产者-消费者模型

什么是生产者-消费者模型?

用一句话概括就是:超市相当于缓存,工厂(生产者)写缓存,顾客(消费者)读缓存。

在整个模型中,换工厂不会影响消费者的购买,超市的商品卖给谁跟工厂也没有关系。因此,超市的存在使得生产者和消费者解耦。解耦后,整个模型支持忙闲不均,工厂生产的内容多了可以存在超市,由超市自主决定如何处理,超市可以让生产者生产慢一点,也可以打促销活动让消费者多消费。整个过程中,超市可以很好的适配生产消费,容错比较大的同时,无需生产者和消费者的直接沟通,全程均由超市来自主控制。

(2)3种关系、2种角色、1个交易场所

①3种关系

生产者和生产者:互斥关系。一家工厂给超市供货时另外的工厂不能供货,防止数据写入冲突。
消费者和消费者:互斥关系。超市只能一个人进,防止读取的数据错乱。
生产者和消费者:互斥 + 同步关系。互斥体现在写数据的时候不能读数据,相当于人家放商品时不能马上去拿。同步体现在排队规则,即工厂和消费者在同一队列里面,一次性放进去一个人,这个人可以是生产者也可以是消费者。

注意生产者和消费者在同意队列里,也就是说超市里面某一时刻永远只能有一个人,要么消费者要么生产者。这里有个性质,当消费者消费之后,就一定有个空位置,这个位置可以留给生产者;同理生产者放完数据后一定知道这里有数据,可以留给消费者。即消费者消费时才知道有没有空位置进货,生产者放了东西之后才知道有没有可供消费的物品。这个性质后面会用到。

②2种角色、1个交易场所

2种角色即生产者和消费者,1个交易场所即中间的缓存。生产者和消费者可以有多个,并且都可以访问缓存,因此缓存需要被保护起来。交易场所不仅仅可以用来传递数据,还能传递任务。 

③“321”原则

生产者-消费者模型一定遵循“321”原则,即3种关系、2种角色、1个交易场所。IPC中的管道就是典型的生产消费模型,线程池的本质也是一个生产者消费者模型,写端为生产者,读端为消费者,中间的管道文件充当两者的缓存,读写端解耦合,都把互相当作文件进行操作。

要实现生产者-消费者模型,其核心就是“321”原则,特别是那个3,需要好好维护。

(3)基于BlockingQueue(阻塞队列)的生产者-消费者模型

线程被存放在阻塞队列里面,缓存为空时,获取数据的线程被阻塞在这个函数里,写数据的线程执行。缓存为满时,存数据的线程被阻塞,写数据的线程执行。当缓存不空不满时,队尾放数据,队头拿数据,这个结构就是基于BlockingQueue(阻塞队列)的生产消费模型,例如管道。

生产者最清楚消费者有没有消费的东西,反之依然。所以实现这个模型需要生产者唤醒消费者,消费者唤醒生产者。如此消费一条,生产一条,实现同步,即排队读写数据。

(4)基于环形队列的生产者-消费者模型

对于环形队列来说,头指针指向队头,尾指针指向队尾的下一个位置,队尾入数据,队头出数据,环形队列为空为满时指针都会指向同一个位置。我们现在就让这个队头代表消费者,队尾代表生产者。生产者在队尾指向的空位置生产数据,消费者在队头消耗数据。

(5)模型的效率

由于生产消费模型的互斥访问,即同一时刻超市永远都只能留一个人,那这个模型的高效从何谈起?

虽然看上去是串行访问,但我们要知道生产者要生产,首先需要原材料;消费者把数据是拿走后,还要做处理。如果完整考虑生产者取数据,消费者处理数据的流程话,这个模型整体的效率就很高。因为当生产者在获取数据时,消费者在处理已有任务;反之消费者在处理数据时,生产者还在超市里面补充任务,整体看来消费者和生产者的执行是并行的。

2.认识信号量

POSIX信号量和system V信号量的本质相同,就是将一块整体的资源分成小份的,分给不同线程使用的计数器。它就像电影院的电影票,资源是电影院的座位,申请sem就相当于预定了个座位,预定后这一小块资源就是该线程的了。

对于信号量来说,申请一块资源要先申请sem,减去一个单位的信号量。申请信号量就是资源的预定机制。二元信号量就是互斥锁,互斥锁就是对一整块资源的预定机制,可以说互斥锁就是信号量的子集。

3.线程安全

(1)死锁

①什么是死锁

形象的说法是,申请别人不会释放的资源,并且自己也不释放资源、僵持不下的状态就叫死锁。举个例子,B申请A的锁,并且说它得到A的锁后就会释放自己的锁;与此同时,A也在申请B的锁,并且说它申请到B的锁后才会释放自己的锁。它们两个线程谁也不让谁,这就陷入了死锁。

单线程也可能产生死锁,一把锁也可能把自己锁住,如连续两次申请锁。当第二次申请锁时,这个线程的右手想要拿左手正在拿着的锁,左手对右手说它需要执行到unlock才会释放,而右手说它马上就要这把锁,不然不让走。这就陷入了死锁。

②死锁的四个必要条件

互斥条件:一块资源每次只能被一个执行流使用
请求与保持条件:保持自己的资源不释放,并且还在请求要别人的资源
不剥夺条件:不会强行把别人的资源给自己,一直两方一直和平地申请,不像系统那样可以直接杀掉进程来得到自己想要的资源
循环等待条件:若干执行流之间形成头尾相接的循环申请资源的关系,两个执行流也能形成环状申请资源的情况。

(2)线程安全和重入

①线程安全

线程安全指的就是多个线程访问公共资源能正常执行,不会互相干扰执行结果,反之就是线程不安全。如不加锁时导致变量为负就是线程不安全。

要保证线程安全,线程一般要设计成单例模式。懒汉(延迟加载,资源利用率高)和饿汉方式。

unique_ptr只在当前代码生效,不涉及线程安全问题,shared_ptr也保证线程安全。

②两种重入的情况

重入有两种情况,一是多线程重入函数,二是信号导致一个执行流重复进入函数。函数不可重入,就有可能引发线程安全问题。

多进程一般不会发生重入,进程间的独立保证使用的变量是存在不同空间里的,一般不会导致问题。

③重入和线程安全的区别

线程安全侧重说明线程访问公共资源的安全问题,表现并发线程的特点,可重入描述的是一个函数能否被重复进入,表示的是函数的特点。

可重入函数是线程安全的一种情况。函数可重入,那就一定线程安全,但线程安全并不一定可重入。举个例子,在一个函数里,如果对临界资源的访问加上锁,则线程安全。但如果在这个加了锁的代码里自我递归,就会导致连续两次申请锁,产生死锁,因此是不可重入的。

malloc、new是不可重入的,也不是线程安全的,STL默认也不是线程安全的。

在STL中,C++/C有在语言级别进行内存管理,如申请一定空间时会多申请,有一些空间用于保存其他信息。这些描述信息被C/C++的库保存起来,是共享的,这些共享的资源会导致线程不安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值