线程同步学习_哲学家问题
2011-06-30 14:06
218 查看
笔者前段时间做一个消息队列优化程序时涉及到多线程同步问题,顺便看了下大学操作系统课程。将生产者-消费者问题以及哲学家就餐问题实现了下,做以下笔记。
哲学家就餐问题:
设有5个哲学家,共享一张放有5把椅子的桌子,每人一把椅子,但是桌子上只有5只筷子,在每人两边分开各放一支;哲学家在就餐时必须试图分两次从两边拾起筷子就餐。
条件:
(1)只有拿到2只筷子,哲学家才能吃饭。
(2)如果筷子在他人手上,哲学家只能等到他人吃完后才能拿到筷子。
(3)任一哲学家在自己未拿到2只筷子吃饭之前,决不放弃自己的筷子。
死锁问题的产生:
每个哲学家都握有自己左边(或都握有右边)的筷子。如果用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只筷子吃饭之前,决不放弃自己的筷子,笔者在网络上看到一种可以放弃自己的筷子的方法。同时定义的信号量是对对应的每一只筷子。当前哲学家在检测左右筷子是否可以拿起的时候,如果不能同时拿起则放弃拿起。然后持续执行一直到左右邻居将筷子放下。我将实现程序贴出:
哲学家就餐问题:
设有5个哲学家,共享一张放有5把椅子的桌子,每人一把椅子,但是桌子上只有5只筷子,在每人两边分开各放一支;哲学家在就餐时必须试图分两次从两边拾起筷子就餐。
条件:
(1)只有拿到2只筷子,哲学家才能吃饭。
(2)如果筷子在他人手上,哲学家只能等到他人吃完后才能拿到筷子。
(3)任一哲学家在自己未拿到2只筷子吃饭之前,决不放弃自己的筷子。
死锁问题的产生:
每个哲学家都握有自己左边(或都握有右边)的筷子。如果用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.使用一个信号量数组(对应每一位哲学家),当一个哲学家左右两个邻居都没有进餐时才可以进入进餐状态,如果所需要的筷子被占用,想进餐的哲学家就会被阻塞。解法多人任意位哲学家都能获得最大并行度。
下面是方法的相关引用,宏定义和全局变量定义:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define PHILOSOPHER_NUM 5 #define THINKING 0 #define HUNGRY 1 #define EATING 2 pthread_mutex_t mutex; sem_t semph[PHILOSOPHER_NUM]; #define LEFT (i + PHILOSOPHER_NUM -1)%PHILOSOPHER_NUM //i 的左领居编号 #define RIGHT (i + 1)%PHILOSOPHER_NUM //i 的右领居编号 int state[PHILOSOPHER_NUM];
程序是在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效果一样。
以下为就餐问题的具体实现:
void think(int i) { } void eat(int i) { } void test(int i) { if((state[i] == HUNGRY ) && (state[LEFT] != EATING) && (state[RIGHT]!= EATING)) { state[i] = EATING; sem_post(&semph[i]); } } void take_forks(int i) { pthread_mutex_lock(&mutex); state[i] = HUNGRY; test(i); pthread_mutex_unlock(&mutex); sem_wait(&semph[i]); } void put_forks(int i) { pthread_mutex_lock(&mutex); state[i] = THINKING; test(LEFT); test(RIGHT); pthread_mutex_unlock(&mutex); } void philosopher(int *index) { int mythreadId = (*index); int sleepTime; bzero(state,0); while(1) { think(mythreadId); take_forks(mythreadId); eat(mythreadId); put_forks(mythreadId); // sleep a random time : between 1 - 5 s sleepTime = 1 + (int)(5.0*rand()/(RAND_MAX+1.0)); usleep(sleepTime*10); } }
这里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函数:
int main() { int i,ret; int threadId[PHILOSOPHER_NUM]; int errNum=0; pthread_t t_phThread[PHILOSOPHER_NUM]; pthread_t t_threadInfo; srand(getpid()); pthread_mutex_init(&mutex,NULL); //ret = pthread_create(&t_threadInfo, NULL, (void *) threadInfo, (void *) NULL); if( ret ) { errNum++; } for(i=0;i<PHILOSOPHER_NUM;i++) { threadId[i] = i; sem_init(&semph[i],0,1); ret=pthread_create(&t_phThread[i], NULL, (void *)philosopher, (void*)(&threadId[i])); if(ret) { errNum++; } usleep(20); } if(errNum) { printf("thread create err! errnum=%d \n",errNum); } sleep(1); }
主函数中主要创建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只筷子吃饭之前,决不放弃自己的筷子,笔者在网络上看到一种可以放弃自己的筷子的方法。同时定义的信号量是对对应的每一只筷子。当前哲学家在检测左右筷子是否可以拿起的时候,如果不能同时拿起则放弃拿起。然后持续执行一直到左右邻居将筷子放下。我将实现程序贴出:
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <semaphore.h> #define PHILOSOPHER_NUM 5 #define THINKING 1 #define HUNGRY 2 #define EATING 3 pthread_mutex_t mutex; sem_t semph[PHILOSOPHER_NUM]; void philosopher(int *index) { int mythreadId; char myState,strState[128]; int leftIndex; int rightIndex; int sleepTime; mythreadId = (*index); leftIndex = mythreadId + PHILOSOPHER_NUM-1)% PHILOSOPHER_NUM; rightIndex = (mythreadId + 1) % PHILOSOPHER_NUM; myState = THINKING; while(1) { switch(myState) { case THINKING: myState = HUNGRY; strcpy(strState,"HUNGRY"); break; case HUNGRY: strcpy(strState,"HUNGRY"); if(!(sem_wait(&semph[leftIndex]))) { if(!(sem_wait(&semph[rightIndex]))) { myState = EATING; strcpy(strState,"EATING"); } else {//不能同时拿起则都不拿起 sem_post(&semph[leftIndex]); } } break; case EATING: sem_post(&semph[leftIndex]); sem_post(&semph[rightIndex]); myState=THINKING; strcpy(strState,"THINKING"); break; } pthread_mutex_lock(&mutex); printf("pholosopher %d begin %s\n",mythreadId,strState); pthread_mutex_unlock(&mutex); sleepTime = 1+(int)(5.0*rand()/(RAND_MAX+1.0)); usleep(sleepTime); } } int main() { int i,ret; int threadId[PHILOSOPHER_NUM]; int errNum=0; pthread_t t_phThread[PHILOSOPHER_NUM]; //pthread_t t_threadInfo; srand(getpid()); pthread_mutex_init(&mutex,NULL); for(i=0;i<PHILOSOPHER_NUM;i++) { threadId[i] = i; sem_init(&semph[i],0,1); ret=pthread_create(&t_phThread[i], NULL, (void *)philosopher, (void*)(&threadId[i])); if(ret) { errNum++; } usleep(20); } if(errNum) { printf("thread create err! errnum=%d \n",errNum); } sleep(1); }
相关文章推荐
- Java总结(十)—实现Runnable接口创建线程,线程安全同步,死锁(哲学家进餐问题),读写锁
- Android(java)学习笔记70:同步中的死锁问题以及线程通信问题
- 操作系统学习笔记(13) 互斥与同步的经典问题 -哲学家进餐问题
- 进程(线程)间同步互斥经典问题(二)哲学家问题
- 线程学习二,notify和wait实现消费者-生产者同步问题
- UNIX程序设计实验六 线程及其同步—哲学家问题
- 实验6线程及其同步—哲学家问题的线程
- 从零学习JAVA多线程(三):线程的同步问题
- Git学习之路(5)- 同步到远程仓库及多人协作问题
- Java线程(八):锁对象Lock-同步问题更完美的处理方式
- Java线程同步问题
- (47)Java学习笔记——多线程 / 线程的安全问题
- JavaSE第一百零四讲:哲学家就餐问题、死锁与使用wait及notify方法实现线程之间的相互通信
- Java多线程——Java线程同步问题
- 0091 Java线程:锁对象lock-同步问题更完美的处理方式【进阶】
- Thread 学习 -同步问题1
- 创建线程的两种方法和同步线程的问题
- Java线程同步问题synchronized
- 线程学习之--7线程的安全问题
- Java新技术---线程学习之常用同步工具类