每个哲学家都握有自己左边(或都握有右边)的筷子。如果用
5
个
信号量
S1~S5
代表
5
个筷子资源,用
Pa~Pe
五个进程代表
5
为哲学家
的话,可以用下述算法描述该死锁情况:
Pa Pb Pc Pd Pe
{P(S1); {P(S2); {P(S3); {P(S4); {P(S5);
P(S5); P(S1); P(S2); P(S3); P(S4);
eat; eat; eat; eat; eat;
V(S1); V(S2); V(S3); V(S3); V(S4);
V(S5;} V(S1);} V(S2);} V(S4);} V(S5);
当且仅当5进程第一轮分别执行完黑线上的操作后(都握有左边筷子),随后才会发生死锁。
降低死锁或者避免死锁情况的出现:
1.哲学家在拿不到第二只筷子时等待随机时间,而不是等待相同时间,死锁的概率就大大降低。
2.拿筷子前,对互斥信号量mutex执行down操作,放回筷子后对mutex执行up操作。理论上可行,但任何时刻都只有一个哲学家在就餐。实际上可以两个同时就餐。
3.使用一个信号量数组(对应每一位哲学家),当一个哲学家左右两个邻居都没有进餐时才可以进入进餐状态,如果所需要的筷子被占用,想进餐的哲学家就会被阻塞。解法多人任意位哲学家都能获得最大并行度。
下面是方法的相关引用,宏定义和全局变量定义:
程序是在Unix环境中编写运行的,<unistd.h>Unix系统标准头文件必不可少,<pthread.h>是线程必须库文件,<semaphore.h>则是信号量操作的头文件。注意还有一个是<sys/sem.h>,区别待查。哲学家共有三个state:THINKING,HUNGRY,EATING。LEFT和RIGHT分别表示当前哲学家的左邻居和右邻居。
信号量的数据类型为结构sem_t,它本质上是一个长整型的数。
初始化信号量函数sem_init()
extern int sem_init __P ((sem_t *__sem, int __pshared, unsigned int __value)); T>
sem为指向信号量结构的一个指针;pshared不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享;value给出了信号量的初始值。
函数sem_post( sem_t *sem )用来增加信号量的值。当有线程阻塞在这个信号量上时,调用这个函数会使其中的一个线程不在阻塞,选择机制同样是由线程的调度策略决定的。它作用是给信号量的值加上一个“1”,它是一个“原子操作”,即同时对同一个信号量做加“1”操作的两个线程是不会冲突的。而同时对同一个文件进行读、加和写操作的两个程序就有可能会引起冲突。
函数sem_wait( sem_t *sem )被用来阻塞当前线程直到信号量sem的值大于0,解除阻塞后将sem的值减一,表明公共资源经使用后减少。sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将会继续执行,介信号量的值将减到1。如果对一个值为0的信号量调用
sem_wait(),这个函数就会地等待直到有其它线程增加了这个值使它不再是0为止。如果有两个线程都在sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。
函数sem_trywait ( sem_t *sem )是函数sem_wait()的非阻塞版本,它直接将信号量sem的值减一。
函数sem_destroy(sem_t *sem)用来释放信号量sem。
笔者这里用sem_t信号量数组对应每一位哲学家,用互斥锁pthread_mutex_t来维护哲学家状态的改变。pthread_mutex_lock()和pthread_mutex_unlock(),这次互斥锁其实很sem_t信号量初始原子值置1效果一样。
以下为就餐问题的具体实现:
这里philosopher()函数是创建的多线程函数,它的参数表示的是第几个线程函数。
函数中while(1)一个就餐操作:
think——>takeforks——>eat——>putforks。
think()和eat()操作这里我们暂不关心。主要是在哲学家takefork和putfork时候所做的互斥操作以及如何通过信号量通知其他线程可以继续操作。
首先任何一个哲学家(ph线程)进行takefork和putfork时候都需要互斥锁mutex控制,保证不会同时有两个哲学家拿起同一个筷子,不然程序进混乱!pthread_mutex_lock(&mutex)锁住,相当于将信号量置0,当别的哲学家(ph线程)希望拿起或者放下fork的时候都会被阻塞。等当前哲学家(ph线程)拿起或者放下fork后,其他哲学家才能继续拿起或者放下fork(当然此处笔者觉得也可以对每个fork对应一个mutex,当前哲学家操作的两个fork置锁,其他的fork对应的mutex可以继续拿起或放下,未具体实现)。当当前哲学家对fork和state改变之后,pthread_mutex_unlock(&mutex)进行开锁。
然后就是每个哲学家对应的信号量semph,takefork时,首先进行test判断当前哲学家的左右邻居是否是EATING状态,如果都不是表示当前哲学家可以EATING(拿起两个筷子),并将当前哲学家对应的信号量sem_post(),做+1操作。takefork结束时,做sem_wait()-1操作,如果哲学家没有拿起筷子,即没有sem_post()操作,那么当前哲学家的semph被sem_wait为0,被阻塞。如果能够EATING,则当前哲学家信号量仍然为1,post和wait抵消。
putfork中EATING完之后则test当前哲学家左右邻居是否能够EATING,如果可以则将他们的semph做sem_post()+1操作,之前被挂起的左右邻居就可以EATING继续操作了。
我们再加上一个main函数:
主函数中主要创建philosopher线程,线程的创建函数如下,
#include<pthread.h>
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
返回值:若成功则返回0,否则返回出错编号。返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于制定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。
笔者同时创建了一个打印信息的线程threadInfo。
主函数的最后sleep()是为了使主函数能停留一会,以便线程运行。当main函数执行结束后,它的线程也会结束。如果不想程序结束,可以在主函数中也添加无限循环,main函数也可以类似一个线程函数持续执行。
这里是方式是哲学家在自己未拿到
2只筷子吃饭之前,决不放弃自己的筷子,笔者在网络上看到一种可以放弃自己的筷子的方法。同时定义的信号量是对对应的每一只筷子。当前哲学家在检测左右筷子是否可以拿起的时候,如果不能同时拿起则放弃拿起。然后持续执行一直到左右邻居将筷子放下。我将实现程序贴出: