多线程编程核心技术(八)管程
处理多线程可以从信号量和管程来进行。Linux就是使用信号量对进行多线程的。
信号量是1965荷兰Dijkstra为了解决并发进程问题 而提出的一个重要操作系统的思想
信号量是操作系统提供的一种协调共享资源访问的方法。和用软件实现的同步比较,软件同步是平等线程间的的一种同步协商机制,不能保证原子性。而信号量则由操作系统进行管理,地位高于进程,操作系统保证信号量的原子性。
信号量是跟锁机制在同一个层次上的编程方法(那么原理是否可以认为是操作隔离性思想)。管程是为了解决信号量在临界区的PV操作上的配对的麻烦,把配对的PV操作集中在一起,生成的一种并发编程方法。
两个或多个进程可以通过简单的信号进行合作,一个进程可以被迫在某个位置停止,直到它接收到一个特定的信号。任何复杂的合作需求都可以通过适当的信号结构 得到满足。为了发信号,需要使用一个称为信号量的特殊变量。为通过信号量s发送信号,进程可执行原语semSignal(s),即V操作;为了通过信号量 s接收信号,进程可执行原语semWait(s),即P操作;如果相应的信号还没有发送,则进程将被挂起,直至发送位置。
信号量思想代码:
semaphore empty=2; //定义empty对应盘子的剩余放水果的位置个数初值为2( 空缓冲区个数 ) semaphore apple=0; //定义信号量apple对应盘子里的苹果数量初值为0 semaphore orange=0; //定义信号量orange对于盘子里的橘子数量初值为0 semaphore mutex=1: //定义信号量mutex来保护盘子被互斥地访问 father(){ //爸爸进程 while(1){ P(empty); //盘子的剩余放水果的位置减一,如果>=0,说明有位置可以放苹果 P(mutex); 在盘子里放一个苹果 V(mutex); V(apple);//盘中苹果数加一 } } mother(){ //妈妈进程 while(1) { P(empty); //盘子的剩余放水果的位置减一,如果>=0,说明有位置可以放橘子 P(mutex); //互斥变量减一,如果<0,则说明有进程在临界区。则当前进程必须等待。 在盘子里放一个橘子 V(mutex); 进程执行完毕,出了临界区,互斥变量值加一。 V(orange); //盘中橘子数加一 } } son(){ //用这段程序产生两个儿子进程 while(1) { P(orange); //盘中橘子个数减一,如果结果>=0时,说明盘中有橘子,可以取 P(mutex); 从盘子里拿一个橘子 V(mutex); V(empty); 取了一个橘子后,盘子的剩余放水果的位置加一 } } daughter(){ //用这段程序产生两个女儿进程 while(1 ) { P(apple); //盘中苹果个数减一,如果结果>=0时,说明盘中有苹果,可以取 P(mutex); 从盘子里拿出一个苹果 V(mutex); V(empty); 取了一个橘子后,盘子的剩余放水果的位置加一 } }
管程可以被认为是一个建筑物,其中包含一个特殊的房间(下图的special room)。该特殊的房间在同一时间只能由一个客户(线程)占用,通常包含一些数据和代码。
如果一个客户想要占据这个特殊房间,他必须先进入走廊(在上图是Hallway,在java中是Entry Set)等待。调度器将根据某些标准(例如FIFO先进先出)来选择一个客户。如果他出于某种原因中止,那么他将被送到等待室(上图wait room),并且在一段时间之后被安排重新进入特殊房间。正如上图所示,这座大楼里包含3间房间。
那么如果信号量为1的情况下,和管程是一样的。
在java内锁的实现是使用管程进行的,管程,对应的英文是 Monitor,所以也可以叫监视器
在管程的发展史上,先后出现过三种不同的管程模型,分别是:Hasen 模型、Hoare 模型和 MESA 模型。
Hasen模型要求notify放到最后,这样T2线程通知T1后,T2线程就结束了,然后T1执行完,这样就能保证同一时刻只有一个线程在执行。
Hoare模型里面,T2线程通知完T1线程后,T2马上阻塞,T1马上执行;等T1执行完之后再唤醒T2线程,也能保证同一时刻只有一个线程在执行,但是T2多了一次阻塞唤醒操作。
MESA模型中,T2唤醒T1之后,T2还是会接着执行,T1并不立即执行,仅仅是从条件变量队列到等待队列中。
这三个模型的共性就是控制了一个时刻只有一个线程can run。思考下为什么需要这样进化。
Hasen的缺点我的理解是:可能会造成CPU放空炮,T1不满住执行条件,又需要重新呼叫T2。
Hoare的缺点:如果T1无法执行。那么会浪费CPU部分资源,但是至少比哈森好,好在如果不满足,可以快速进行调度
MESA的模型结合了上面两个的特点。缺点就是时刻不再固定,T1瞬息万变可能需要进行重新的条件判断——>乐观思想?
多线程有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。
同步是指线程之间在时间上的步调协调,并不一定会涉及到共享资源的互斥操作。比如线程1完成了自己的步骤1之后,要先等待线程2也完成了自己的步骤1,线程1才能进行自己的步骤2,这过程中线程1和线程2之间不一定有共享资源存在。
就好比生活中的同学聚会,去聚会地点(步骤1),开跑车去的同学1先到达,步行去的同学2晚到达,但是同学1必须要等同学2也到达后(完成步骤1),才能进入聚会的吃饭环节(步骤2)。
管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来(就像上面的小房子只能一个人)。假如我们要实现一个线程安全的阻塞队列,一个最直观的想法就是:将线程不安全的队列封装起来,对外提供线程安全的操作方法,例如入队操作和出队操作,不过实际上有点捞。
管程其实更加像是面向对象的一种设计,你不安全是吧,那我就把你关起来,弄个小窗口和你交流。控制你的活动同时也规范了外部的活动。
至于同步就有点困难,让每个条件变量都对应有一个等待队列,如下图,条件变量 A (A>100)和条件变量 B(C>200,A>1000) 分别都有自己的等待队列。
当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。这个过程类似就医流程的分诊,只允许一个患者就诊,其他患者都在门口等待。
Java 内置的管程方案(synchronized)使用简单,synchronized 关键字修饰的代码块,在编译期会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而 Java SDK 并发包实现的管程支持多个条件变量,不过并发包里的锁,需要开发人员自己进行加锁和解锁操作。
总结起来就是,管程就是一个对象监视器。任何线程想要访问该资源,就要排队进入监控范围。进入之后,接受检查,不符合条件(例如"火车进了隧道,发生全是汽车,那么就先让别的车先走"),则要继续等待,直到被通知,然后继续进入监视器。