经典同步问题
哲学家就餐问题
先来看看哲学家就餐的问题描述:
5 个⽼⼤哥哲学家,闲着没事做,围绕着⼀张圆桌吃⾯;
巧就巧在,这个桌⼦只有 5 ⽀叉⼦,每两个哲学家之间放⼀⽀叉⼦;
哲学家围在⼀起先思考,思考中途饿了就会想进餐;
奇葩的是,这些哲学家要两⽀叉⼦才愿意吃⾯,也就是需要拿到左右两边的叉⼦才进餐;
吃完后,会把两⽀叉⼦放回原处,继续思考;
那么问题来了,如何保证哲 学家们的动作有序进⾏,⽽不会出现有⼈永远拿不到叉⼦呢?
方案一
我们⽤信号量的⽅式,也就是 PV 操作来尝试解决它,代码如下:
上⾯的程序,好似很⾃然。拿起叉⼦⽤ P 操作,代表有叉⼦就直接⽤,没有叉⼦时就等待其他哲学家放回叉⼦。
不过,这种解法存在⼀个极端的问题:假设五位哲学家同时拿起左边的叉⼦,桌⾯上就没有叉⼦了,这样就没有⼈能够拿到他们右边的叉⼦,也就说每⼀位哲学家都会在 P(fork[(i + 1) % N ]) 这条语句阻塞了,很明显这发⽣了死锁的现象。
方案二
既然「⽅案⼀」会发⽣同时竞争左边叉⼦导致死锁的现象,那么我们就在拿叉⼦前,加个互斥信号量,代码如下:
上⾯程序中的互斥信号量的作⽤就在于,只要有⼀个哲学家进⼊了「临界区」,也就是准备要拿叉⼦时,其他哲学家都不能动,只有这位哲学家⽤完叉⼦了,才能轮到下⼀个哲学家进餐。
⽅案⼆虽然能让哲学家们按顺序吃饭,但是每次进餐只能有⼀位哲学家,⽽桌⾯上是有 5 把叉⼦,按道理是能可以有两个哲学家同时进餐的,所以从效率⻆度上,这不是最好的解决⽅案。
方案三
那既然⽅案⼆使⽤互斥信号量,会导致只能允许⼀个哲学家就餐,那么我们就不⽤它。
另外,⽅案⼀的问题在于,会出现所有哲学家同时拿左边⼑叉的可能性,那我们就避免哲学家可以同时拿左边的⼑叉,采⽤分⽀结构,根据哲学家的编号的不同,⽽采取不同的动作。
即让偶数编号的哲学家「先拿左边的叉⼦后拿右边的叉⼦」,奇数编号的哲学家「先拿右边的叉⼦后拿左边的叉⼦」。
上⾯的程序,在 P 操作时,根据哲学家的编号不同,拿起左右两边叉⼦的顺序不同。另外,V 操作是不需要分⽀的,因为 V 操作是不会阻塞的。
⽅案三即不会出现死锁,也可以两⼈同时进餐。
方案四
在这⾥再提出另外⼀种可⾏的解决⽅案,我们⽤⼀个数组 state 来记录每⼀位哲学家在进程、思考还是饥饿状态(正在试图拿叉⼦)。
那么,⼀个哲学家只有在两个邻居都没有进餐时,才可以进⼊进餐状态。
第 i 个哲学家的左邻右舍,则由宏 LEFT 和 RIGHT 定义:
LEFT : ( i + 5 - 1 ) % 5
RIGHT : ( i + 1 ) % 5
⽐如 i 为 2,则 LEFT 为 1, RIGHT 为 3。
具体代码实现如下:
上⾯的程序使⽤了⼀个信号量数组,每个信号量对应⼀位哲学家,这样在所需的叉⼦被占⽤时,想进餐的哲学家就被阻塞。
注意,每个进程/线程将 smart_person 函数作为主代码运⾏,⽽其他 take_forks 、 put_forks 和test 只是普通的函数,⽽⾮单独的进程/线程。
⽅案四同样不会出现死锁,也可以两⼈同时进餐。
读者-写者问题
前⾯的「哲学家进餐问题」对于互斥访问有限的竞争问题(如 I/O 设备)⼀类的建模过程⼗分有⽤。
另外,还有个著名的问题是「读者-写者」,它为数据库访问建⽴了⼀个模型。
读者只会读取数据,不会修改数据,⽽写者即可以读也可以修改数据。
读者-写者的问题描述:
「读-读」允许:同⼀时刻,允许多个读者同时读
「读-写」互斥:没有写者时读者才能读,没有读者时写者才能写
「写-写」互斥:没有其他写者时,写者才能写
接下来,提出⼏个解决⽅案来分析分析。
方案一:读者优先
使⽤信号量的⽅式来尝试解决:
信号量 wMutex :控制写操作的互斥信号量,初始值为 1 ;
读者计数 rCount :正在进⾏读操作的读者个数,初始化为 0;
信号量 rCountMutex :控制对 rCount 读者计数器的互斥修改,初始值为 1;
接下来看看代码的实现:
上⾯的这种实现,是读者优先的策略,因为只要有读者正在读的状态,后来的读者都可以直接进⼊,如果读者持续不断进⼊,则写者会处于饥饿状态。
方案二:写者优先
那既然有读者优先策略,⾃然也有写者优先策略:
只要有写者准备要写⼊,写者应尽快执⾏写操作,后来的读者就必须阻塞;
如果有写者持续不断写⼊,则读者就处于饥饿;
在⽅案⼀的基础上新增如下变量:
信号量 rMutex :控制读者进⼊的互斥信号量,初始值为 1;
信号量 wDataMutex :控制写者写操作的互斥信号量,初始值为 1;
写者计数 wCount :记录写者数量,初始值为 0;
信号量 wCountMutex :控制 wCount 互斥修改,初始值为 1;
具体实现如下代码:
注意,这⾥ rMutex 的作⽤,开始有多个读者读数据,它们全部进⼊读者队列,此时来了⼀个写者,执⾏了 P(rMutex) 之后,后续的读者由于阻塞在 rMutex 上,都不能再进⼊读者队列,⽽写者到来,则可以全部进⼊写者队列,因此保证了写者优先。
同时,第⼀个写者执⾏了 P(rMutex) 之后,也不能⻢上开始写,必须等到所有进⼊读者队列的读者都执⾏完读操作,通过 V(wDataMutex) 唤醒写者的写操作。
⽅案三:公平策略
既然读者优先策略和写者优先策略都会造成饥饿的现象,那么我们就来实现⼀下公平策略。
公平策略:
优先级相同;
写者、读者互斥访问;
只能⼀个写者访问临界区;
可以有多个读者同时访问临街资源;
具体代码实现:
看完代码不知你是否有这样的疑问,为什么加了⼀个信号量 flag ,就实现了公平竞争?
对⽐⽅案⼀的读者优先策略,可以发现,读者优先中只要后续有读者到达,读者就可以进⼊读者队列, ⽽写者必须等待,直到没有读者到达。
没有读者到达会导致读者队列为空,即 rCount==0 ,此时写者才可以进⼊临界区执⾏写操作。
⽽这⾥ flag 的作⽤就是阻⽌读者的这种特殊权限(特殊权限是只要读者到达,就可以进⼊读者队列)。
⽐如:开始来了⼀些读者读数据,它们全部进⼊读者队列,此时来了⼀个写者,执⾏ P(flag) 操作,使得后续到来的读者都阻塞在 flag 上,不能进⼊读者队列,这会使得读者队列逐渐为空,即 rCount 减为0。
这个写者也不能⽴⻢开始写(因为此时读者队列不为空),会阻塞在信号量 wDataMutex 上,读者队列中的读者全部读取结束后,最后⼀个读者进程执⾏ V(wDataMutex) ,唤醒刚才的写者,写者则继续开始进⾏写操作。
学自小林coding,侵删