目录
(3)基于BlockingQueue(阻塞队列)的生产者-消费者模型
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++的库保存起来,是共享的,这些共享的资源会导致线程不安全。
382

被折叠的 条评论
为什么被折叠?



