在学习到这一块内容的时候,尤其是关于信号量机制(P、V操作)的内容时,我总感觉缺少一种可称呼为algorithm的东西将这些操作以适当的方式组织起来。面对一些经典问题时我们或许可以靠死记硬背来解答,但这显然不适合处理那些“新颖的题型”。中文网络中我也没看到有什么合适的方案。鉴于此,经本人研究,我感觉自己构想出了合适的步骤来回答这类问题。
处理P、V操作最大的困难在于交互性(intersubjectivity)的分析。 一般情况下,我们写代码时考虑所关注的对象有何种属性;但对于进程同步问题而言,进程本身的属性是不重要的,它与哪些资源产生关联才是重要的——这些关联正是信号量机制所调节的对象。
举个例子。有一个经典的生产者-消费者问题是说,桌上有一个最多只能放1个水果的盘子。爸爸专门往盘子里放苹果,妈妈专门往盘子里放橘子;儿子专吃盘子里的橘子,女儿专吃盘子里的苹果。盘子为空时才能放水果,盘子里有水果时、水果才能被取走。请用信号量机制描述四人的进程。
这个问题涉及的对象包括4个人和1个盘子,各对象之间的关联可以用以下方式来描绘:
其中,带箭头的线就代表一种关系。我们有理由断言:凡是存在a->b的关系(如father->plate),那么必有b->a的关系(如plate->father)。这是因为a对b所做的动作不是无限制的,它必然受到b对a的反馈的调节。(在这个例子中,就是plate的容量制约了father放水果的动作)
在a与b的交互中,如果我们站在a的视角,那么可以把a->b的关系定义为一种主动关系,而b->a则定义为被动关系。我们可假定这种关系是可传递的,即(站在a的视角)a->b与b->c可以推出a->c,且a->c是a的主动关系;类似的,b->a和c->b可推出c->a,且c->a是a的被动关系。(不否认自反性的可能,但这要根据实际情形做斟酌)
由于关系a->b与b->a总是相互受到制约(至少在进程同步问题里总是如此),我们可以为这一对关系赋予相同的名称。不同对的关系的名称可能相同也可能不同,取决于双方之间的数量变化。名称可以是毫无意义的。如下图所示。
结合题设,当father对plate发出主动关系active(f)时,女儿一方就能得到一个被动关系passive(d),同时mother将无法发出主动关系active(m)。反之,mother发出active(m)时,儿子获得了passive(s),而父亲此时无法发出active(f)。当女儿发出active(d),或儿子发出active(s)时,父亲和母亲均能获得被动关系active(f)、active(m),使得他们再一次发出主动关系得以成为现实可能。因此f和m是同一对关系,即f=m。上图可以被简化为以下。
直觉敏锐的读者此时就能发现,p、d、s其实就是本例需要设置的三个信号,分别指向盘子的容量、苹果的数量和橘子的数量。
信号的值如何确定?只要考察在最开始时、各方能够发出的主动关系的数量即可。在最开始,父亲或母亲都可以发出动作active(p),而女儿不能发出active(d)、儿子不能active(s)。(按题设,盘子一开始是空的)所以p的初始值为1,d和s的初始值均为0 。
既然信号的设定已经实现,那么如何编入到进程之中呢?这里不妨定义一个名叫“视域”的概念。所谓“视域”就是某一对象的全部主动关系的一个子集,这个子集中任意一个关系的目标(或者也可以说是“客体”)都是该对象(也可以说是“主体”)所要影响的目标,也即主体欲施加于变化的客体;而该子集之外不存在能满足上述条件的关系。借助这个具有强烈直观色彩的概念,我们可以说四个进程(即father,mother,daughter,son)所涉及的信号量都涉及且只涉及视域内的主动关系。如下图所示,我用不同颜色来标识视域的不同。
这样就能敲定最终答案了,如以下所示。聪明的读者应该很快就能发现,主动关系意味着信号量机制里的P,而被动关系意味着信号量机制里的V 。
semaphore p=1,d=0,s=0;
cobegin
{
Process father(){
while(1){
//prepare an apple
P(p);
//put the apple on the plate
V(d);
}
}
Process mother(){
while(1){
//prepare an orange
P(p);
//put the orange on the plate
V(s);
}
}
Process son(){
while(1){
P(s);
//take the orange from the plate
V(p);
//eat the orange
}
}
Process daughter(){
while(1){
P(d);
//take the apple from the plate
V(p);
//eat the apple
}
}
}
coend
如果你对上述方法有了一定的了解,那么可以用其它例题来强化理解。这里选择经典的读者-写者问题。则此类图示可以表示为如下,这是一个读者优先的解决方案。
在本图中,我们认为“主体”就是每一个writer和reader。由此可以得到敲定以下答案。
int count=0;
semaphore rw_mutex=1,ct_mutex=1;
cobegin
{
Process writer(){
P(rw_mutex);
//Writing
V(rw_mutex);
}
Process reader(){
P(ct_mutex);
if(count==0) P(rw_mutex);
count++;
V(ct_mutex);
//Reading
P(ct_mutex);
count--;
if(count==0) V(rw_mutex);
V(ct_mutex);
}
}
coend
如果希望每个进程都能公平地接触到文件,那么只要按下图方式调整交互关系即可,代码略。需要注意的是active(p_mutex)在读进程手中不能构成任何制约(即在reading之前就实施passive(p_mutex)),但在写进程手中可以锁定当前访问文件的进程数量(即完成写操作后再实施passive(p_mutex))。
我们可以看到,对被动关系的设定要比对主动关系的设定往往有更大的自由度,尽管这并不意味着完全不考虑顺序。一个主体对于其所有的主动关系,他可以借助于传递性而“看”到对应的被动关系;也可以对其一无所知。因此我们的作图必须要非常审慎,如此才能不产生任何误导。一般来说,如果客体是一个不受人控制的“单独物体”,那么主体对其发出主动关系时,他大约可以看到被动关系;反之,基本上就看不到了。
我认为本方法作为一种辅助手段,然能极大地对题目分析产生助益。尽管如此,若使用不当,还是很容易会造成错误的。
以2011年某统考真题为例。某银行提供1个服务窗口和10个供顾客等待的座位。顾客到达银行时,若有空座位,则在取号机上取号,并于座位上等待叫号。取号机同时只能被1名顾客操作。当营业员空闲时,则通过叫号选取1为顾客,并为其服务。
对于顾客(customer)和营业员(clerk)两个进程,我们可以画出以下图示。
答案如下。
semaphore a=10, b=1, c=0, d=0;
cobegin{
Process Customer(){
P(a);
//While there's a seat
P(b);
V(b);
V(c);
P(d);
//being serviced
}
Process Clerk(){
while(1){
P(c);
V(a);
V(d);
}
}
}
coend
由于完全采用有向图的方式来考察各种交互行为的数量关系,因此“意义”就被屏蔽了。读者们从以上例子中可以发现,信号的名字对于我们分析问题而言已经变得无关紧要,或者说甚至就是一个累赘。尽管如此,为了使图示契合题意,信号的现实意义也是需要考虑的。
不难发现同一个题目可以给出多个图示,从而得出不同的答案。这些答案中有的简洁,有的则繁琐。不管美观上如何,个人认为能把题目做出来才是最关键的。