![c253ede910aea96c966c0975bd6044cb.png](https://i-blog.csdnimg.cn/blog_migrate/6c75e3b93b9c97bb10c513c7ada33077.jpeg)
问题提出
我不止一次地强调,计算机的资源是有限的,但人们会用大量任务压榨计算机。遗憾的是,对于那些十分有限的资源,这些任务不能同时调用它们,需要协调。(打个无伤大雅的比方,就像卫生间坑位有限,需要协调一样,你肯定无法想象多人共用一个坑位吧)。总而言之,协调好资源就是一种同步(让大家都把自己的“时钟”调到资源那儿)。
问题解决
如果卫生间没有坑位了,怎么办?等呗。如果等到坑位,就可以使用了(注意要把门关上)。最后,使用完毕要冲掉,然后释放坑位资源。
在代码层面,其实所谓的“P/V”操作就是这里的“等待”(wait())和“释放”(signal())。
在使用前,要等待有空闲资源,就是P()一下,或者说wait()一下;当等来资源的时候,就进入了程序的“临界区”,独自使用资源;使用完资源之后,就得释放资源,就是V()一下,或者说signal()一下(就像高铁上的灯指示一下,更新一下可用资源数量)。
经典案例
哲学家进餐问题。设想一张桌子上有5位哲学家,他们只做两件事:拿起筷子吃饭或者放下筷子思考,他们不会一边吃饭一边思考。有趣之处在于,桌子上共有5根筷子,与哲学家一起排成一个环,每位哲学家双手边都各有一根筷子。如下图:
![2e4f038f682397bd0748c6d5c0ae99d7.png](https://i-blog.csdnimg.cn/blog_migrate/66534fd8aea7c665184551976c6b7d28.png)
怎样用代码描述哲学家吃饭和思考的过程而不引起冲突{同时使用一根筷子}呢?让我们试着解决这个同步问题。
- 共用的资源是什么?
很明显是筷子啊。但是这跟日常上厕所的问题不太一样,平时有个坑就能去,但在这里,只能用双手边的筷子,不能拿别人那儿的筷子。(可以拿邻居的。。)所以事实上,筷子和筷子之间是不同的,我们要给每根筷子一个信号量(就是一个数,类比卫生间门口有个指示坑位的数字),这个信号量的初始值是1,因为每根筷子都是独一无二的。 - 有哪些活动?相关约束是什么?
吃饭和思考,吃饭前要拿筷子,吃完之后放下筷子;思考前要放下筷子,思考后等着下一顿饭。
于是我们就很快写出如下代码:
semaphore chopsticks[5]={1,1,1,1,1};
void Philosopher_i(){//i号哲学家的进程
do{
wait(chopsticks[i]);//等左手边的筷子
wait(chopsticks[(i+1)%5]);//等右手边的筷子
eat();//吃饭
signal(chopsticks[(i+1)%5]);//放下右手的筷子
signal(chopsticks[i]);//放下左手的筷子
think();//思考
}while(true);//像个没有感情的机器人继续循环
}
这段代码cover了前面讲过的所有要点,并且能够保证不产生冲突。
但是!你发现没有,在这种同步机制下,可能出现哲学家们没法吃饭、没法思考、面面相觑的情况。试想某个时刻,所有人都拿起了自己左手边的筷子,桌子上没有筷子能拿了,也没有人会放下,而每个人此时只有一根筷子,没法吃饭也没法思考——大家都懵了!“太默契了吧小老弟”
这种情况不算冲突,但是属于异常情形(术语叫“死锁”),所以要避免。如何避免呢?有很多方法,以下介绍最简单的一种。
我们先想想,什么时候会出现上面的死锁?在刚开始的时候,大家同时运行代码中的第一行"wait(chopsticks[i])"把左手边的筷子拿了起来,这就产生了死锁。那么,如果限定某一时刻桌子上只有一位哲学家能拿筷子,问题岂不就解决了?对的。我们不妨把这种拿筷子的权限抽象成一个单位的独占资源,这样就能很快写出如下代码:
semaphore chopsticks[5]={1,1,1,1,1};
semaphore mutex=1;//互斥量(抽象的独占资源)
void Philosopher_i(){//i号哲学家的进程
do{
wait(mutex);//等到只有我能拿筷子
wait(chopsticks[i]);//等左手边的筷子
wait(chopsticks[(i+1)%5]);//等右手边的筷子
signal(mutex);//我拿完筷子了,让别人拿吧
eat();//吃饭
signal(chopsticks[(i+1)%5]);//放下右手的筷子
signal(chopsticks[i]);//放下左手的筷子
think();//思考
}while(true);//像个没有感情的机器人继续循环
}
这种解决办法还算漂亮,但并不完美,因为那么大的桌子,每次只让一个人拿筷子,效率也太低了吧,事实上相隔较远的哲学家并不会产生冲突,可这样子处理也把他们给限制了。有没有更优美的解决方案呢?
当然有。不知道大家记不记得以前学组合数学的时候有个鸽巢原理(或者叫抽屉原理),就是说n+1只相同的袜子放在n个抽屉里,则至少有一个抽屉里能凑成一双。很好理解吧,考虑最坏情况——即每个抽屉里只有一只袜子,这样一共n只袜子,还有一只袜子不管放在哪个抽屉里都能凑成一双了。
如果我们把筷子当成袜子,最多几个人同时拿这5只筷子的时候,至少有一个人能拿到一双筷子呢?很显然答案是4。我们就把这个4当作拿筷子的名额,很容易写出代码如下:
semaphore chopsticks[5]={1,1,1,1,1};
semaphore mutex=4;//互斥量(拿筷子的名额)
void Philosopher_i(){//i号哲学家的进程
do{
wait(mutex);//等我抢到一个拿筷子的名额
wait(chopsticks[i]);//等左手边的筷子
wait(chopsticks[(i+1)%5]);//等右手边的筷子
signal(mutex);//我拿完筷子了,释放一个名额
eat();//吃饭
signal(chopsticks[(i+1)%5]);//放下右手的筷子
signal(chopsticks[i]);//放下左手的筷子
think();//思考
}while(true);//像个没有感情的机器人继续循环
}
其实这段代码和前一段代码的差别只有第二行,但在实际运行的时候,情况可大不相同。如果哲学家人数增多,显然后面的程序效率更高。
总结
为了更好地实现共享,必须解决“同步”和“互斥”这两种关系。以上案例展示了经典的处理方案,如果用更复杂的信号量机制,也可以有其他处理办法,但超出了目前考研的要求。感兴趣的朋友可以找来相关论文和教材进行研究。