经典进程同步问题(二)——哲学家进餐问题

目录

一、问题描述

二、解题思路:

三、死锁问题解决

3.1  方案一(引入记录型信号量)

3.2  方案二(引入AND信号量)

3.3  方案三

四、源码

4.1  方案一(记录型信号机制)

4.2  方案二(AND型信号机制)

4.3  方案三

扫描二维码关注公众号,回复: 17365403 查看本文章

五、信号量机制

5.1  整型信号量

5.2  记录型信号量

5.3  AND型信号量


一、问题描述

        该问题描述的是五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替的进行思考和进餐。平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。

二、解题思路:

        经过分析可知,放在桌子上的5根筷子属于临界资源,在一段时间内只允许一位哲学家使用。为了实现对筷子的互斥使用,可以用一个信号量来表示一根筷子,由这5个信号量组成一个信号量数组。其描述如下:

semaphore chopstick[5]={1,1,1,1,1);

所有的信号量初始化为1,第 i 位哲学家的行为可描述如下:

while(1)
{
    think();
    hungry();
    wait(chopstick[i]);
    wait(chopstick[(i+1)%5]);
    eat();
    signal(chopstick[i]);
    signal(chopstick[(i+1)%5]);
}

因为筷子是临界资源,因此当一个线程在使用某根筷子的时候,应该给这根筷子加锁,使其不能被其他进程使用。

根据以上分析,可以使用pthread_create函数创建五个线程,可以使用pthread_mutex_t chopstick[5]表示有五根筷子,五个不同的临界资源,并用pthread_mutex_init(&chopstick[i], NULL);来初始化他们。

按照上述的解决方案,确实可以有效解决互斥使用筷子的问题,但是同时也可能造成死锁。假如5个哲学家同时饥饿而各自拿起自己左边的筷子,这时每个人右边的筷子都不存在了,从而进入无限期的等待。

三、死锁问题解决

3.1  方案一(引入记录型信号量)

        至多只允许4个哲学家同时拿去左边的筷子,最终就能够保证至少有一位哲学家能够进餐,并在进餐完毕后释放他用过的筷子,从而使得更多的哲学家进餐。因此,再设一个信号量num,使其初值为4,每当有一名哲学家拿起左边的筷子时,num减一,当其释放筷子时,num加一。(引入P、V操作)

具体呈现如下:

void philosopher (void* arg) {
    while (1) {
        think();
        hungry();
        P(&num);//C语言提供的P操作的函数是sem_wait
        wait(chopsticks[i]);
        wait(chopsticks[(i+1)%5]);
        eat();
        signal(chopsticks[i]);
        V(&num);//C语言提供的V操作的函数是sem_post
        signal(chopsticks[(i+1)%5]);
    }
}

3.2  方案二(引入AND信号量)

仅当哲学家左右两只筷子同时可用时,才允许他拿起筷子。即:要求每个哲学家先获得两个临界资源后方能进餐;这在本质上就是AND信号量机制。

while (1) {
    think();
    hungry();

    Swait(chopsticks[i],chopsticks[(i+1)%5]);
    
    eat();

    Signal(chopsticks[i],chopsticks[(i+1)%5]);   
}

AND信号量的实质就是锁住全部的临界资源,当一个哲学家企图拿筷子的时候,就将所有的资源锁住,然后让他去拿他需要的筷子,等他取到他需要的筷子之后,就解锁,然后让其他哲学家取筷子。由于C语言库中并没有Swait的相关函数,因此,我们利用全局互斥变量来完成这一目的。

3.3  方案三

规定:奇数哲学家先拿去左边的筷子,偶数哲学家先拿起右边的筷子;因此,1、2号哲学家竞争1号筷子,3、4号哲学家竞争3号筷子......即:五位哲学家都先竞争奇数号筷子,获得后再去竞争偶数号筷子,最后总会有一位哲学家能够获得两根筷子进餐。

while (1)
{
    think();
    hungry();
    if (i % 2 == 0) 
    {//偶数哲学家,先右后左
        wait(chopsticks[(i+1)%5]);
        wait(chopsticks[i]);
        eat();
        signal(chopsticks[i]);
        signal(chopsticks[(i+1)%5]);
    }
    else
     {//奇数哲学家,先左后又
        wait(chopsticks[i]);
        wait(chopsticks[(i+1)%5]);
        eat();
        signal(chopsticks[(i+1)%5]);
        signal(chopsticks[i]);
     }
}

四、源码

4.1  方案一(记录型信号机制)

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>

#define TRUE  1
#define FALSE 0
#define N 	  5	

int philosophers[N]={0,1,2,3,4};
int num=4;

struct data             //信号量结构体
{
	sem_t chopstick[N];
	pthread_mutex_t chops[N];
}sem;

void delay()
{
	int time = rand() % 10 + 1;          //随机使程序睡眠0点几秒           
	usleep(time * 100000);
}

void think(int i)
{
	
	printf("philosopher%d is thinking!\n",i);
} 
 
void hungry(int i)
{
	printf("philosopher%d is hungry!\n",i);
}

void eat(int i)
{
	printf("philosopher%d is eating!\n",i);
}

void philosopher(void *arg)
{
	int i=*(int *)arg;
	int a=10;
	while(a>=0)
	{
		think(i);//思考状态 
		delay();//延迟 
		hungry(i);//饥饿状态
		 
		if(num>=0)
		{
			num--;//P操作 
			sem_wait(&sem.chopstick[i]);//取左边筷子 
			sem_wait(&sem.chopstick[(i+1)%5]);//取右边筷子 
		
			eat(i);//就餐 
			
			sem_post(&sem.chopstick[i]);//放回左边筷子 
			sem_post(&sem.chopstick[(i+1)%5]);//放回右边筷子 
			printf("ready!\n");
			a--;
		}
		else
		{
			printf("chopsticks is not enough!\n"); 
			a--;
		}
		
		num++;//V操作 
		
	}
}

int main()
{
	int i;
	pthread_t phd[N];
	srand((unsigned int)time(NULL));
	
	for(i=0;i<5;i++)//信号量初始化 
	{
		sem_init(&sem.chopstick[i],0,1);
	}

	for(i=0;i<5;i++)//创建线程 
	{
		pthread_create(&phd[i],NULL,(void *)philosopher,&philosophers[i]);
	} 
	
	for(i=0;i<5;i++)//线程等待 
	{
		pthread_join(phd[i],NULL); 
	}
	
	for(i=0;i<5;i++)//信号量销毁 
	{
		sem_destroy(&sem.chopstick[i]);
	}

	return 0;
}

4.2  方案二(AND型信号机制)

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>

#define TRUE  1
#define FALSE 0
#define N 	  5	

int philosophers[N]={0,1,2,3,4};
pthread_mutex_t mutex;	//互斥变量使用特定的数据类型 
struct data             //信号量结构体
{
	sem_t chopstick[N];	
}sem;

void delay()
{
	int time = rand() % 10 + 1;          //随机使程序睡眠0点几秒           
	usleep(time * 100000);
}

void think(int i)
{
	
	printf("philosopher%d is thinking!\n",i);
} 
 
void hungry(int i)
{
	printf("philosopher%d is hungry!\n",i);
}

void eat(int i)
{
	printf("philosopher%d is eating!\n",i);
}

void philosopher(void *arg)
{
	int i=*(int *)arg;
	int a=2;
	while(a>=0)
	{
		think(i);//思考状态 
		delay();//延迟 
		hungry(i);//饥饿状态
		
		pthread_mutex_lock(&mutex);    //互斥锁上锁
		sem_wait(&sem.chopstick[i]);//取左边筷子 
		sem_wait(&sem.chopstick[(i+1)%5]);//取右边筷子 
		
		eat(i);//就餐
			
		sem_post(&sem.chopstick[i]);//放回左边筷子
		sem_post(&sem.chopstick[(i+1)%5]);//放回右边筷子
		
		printf("ph%d ENding!\n",i);
		pthread_mutex_unlock(&mutex);  //互斥锁解锁
		a--;
	}
}

int main()
{
	int i;
	pthread_t phd[N];
	srand((unsigned int)time(NULL));
	
	for(i=0;i<5;i++)//信号量初始化 
	{
		sem_init(&sem.chopstick[i],0,1);
	}
	
	pthread_mutex_init(&mutex, NULL);  //互斥锁初始化
	
	for(i=0;i<5;i++)//创建线程 
	{
		pthread_create(&phd[i],NULL,(void *)philosopher,&philosophers[i]);
	} 
	
	for(i=0;i<5;i++)//线程等待 
	{
		pthread_join(phd[i],NULL); 
	}
	
	for(i=0;i<5;i++)//信号量销毁 
	{
		sem_destroy(&sem.chopstick[i]);
	}
	pthread_mutex_destroy(&mutex);   //互斥锁的销毁

	return 0;
}

4.3  方案三

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <string.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>

#define TRUE  1
#define FALSE 0
#define N 	  5	

int philosophers[N]={0,1,2,3,4};

struct data             //信号量结构体
{
	sem_t chopstick[N];
}sem;

void delay()
{
	int time = rand() % 10 + 1;          //随机使程序睡眠0点几秒           
	usleep(time * 100000);
}

void think(int i)
{
	printf("philosopher%d is thinking!\n",i);
} 
 
void hungry(int i)
{
	printf("philosopher%d is hungry!\n",i);
}

void eat(int i)
{
	printf("philosopher%d is eating!\n",i);
}

void philosopher(void *arg)
{
	int i=*(int *)arg;
	int a=2;
	while(a>0)
	{
		think(i);//思考状态 
		delay();//延迟 
		hungry(i);//饥饿状态
		
		if(i%2==0)//偶数哲学家,先右后左 
		{
			sem_wait(&sem.chopstick[(i+1)%5]);//取右边筷子
			sem_wait(&sem.chopstick[i]);//取左边筷子 
			
			eat(i);//就餐
				
			sem_post(&sem.chopstick[i]);//放回左边筷子
			sem_post(&sem.chopstick[(i+1)%5]);//放回右边筷子
			printf("pholosopher%d ending\n",i);
		}
		else//奇数哲学家,先左后右 
		{
			sem_wait(&sem.chopstick[i]);//取左边筷子
			sem_wait(&sem.chopstick[(i+1)%5]);//取右边筷子 
			
			eat(i);//就餐
			
			sem_post(&sem.chopstick[(i+1)%5]);//放回右边筷子
			sem_post(&sem.chopstick[i]);//放回左边筷子
			printf("pholosopher%d ending\n",i);
		}
		a--;
	}
}

int main()
{
	int i;
	pthread_t phd[N];
	srand((unsigned int)time(NULL));
	
	for(i=0;i<5;i++)//信号量初始化 
	{
		sem_init(&sem.chopstick[i],0,1);
	}
	
	for(i=0;i<5;i++)//创建线程 
	{
		pthread_create(&phd[i],NULL,(void *)philosopher,&philosophers[i]);
	} 
	
	for(i=0;i<5;i++)//线程等待 
	{
		pthread_join(phd[i],NULL); 
	}
	
	for(i=0;i<5;i++)//信号量销毁 
	{
		sem_destroy(&sem.chopstick[i]);
	}

	return 0;
}

五、信号量机制

        信号量机制(Semaphores)是荷兰学者Dijkstra提出的一种卓有成效的进程同步工具。整型信号量和记录型信号量都适用于多个进程仅共享一个临界资源的情况,而AND型信号量则适用于多个进程共享多个临界资源的情况。

5.1  整型信号量

        最初由Dijkstra把整型信号量定义为一个用于表示资源数目的整型量S,该整型量除初始化外仅通过两个原子操作:wait()、signal()。通常称这两个操作分别为P、V操作。具体描述如下:

wait(S)
{
    while(S<=0);//只有当不满足循环条件时(即:S>0时),才跳出循环,执行后面的语句。
    S--;
}

signal(S)
{
    S++;
}

所谓的原子操作,即该语句在执行过程中不可中断。

5.2  记录型信号量

        在上面的描述中,我们可以发现:若S<=0,则会不停地循环,使得进程处于“忙等”状态。因此,我们引入“让权等待”机制,同时为了避免多个进程访问同一临界资源的情况,我们添加了一个进程链表指针list,用于链接上述所有等待进程。具体描述如下:

typedef struct
{
    int value;    //初值表示资源数目,每次对其进行wait操作,表示进程请求一个单位的该类资源    
    struct process_control_block *list;
}semaphore;

wait(semaphore *S)
{
    S->value--;
    if(S->value<0)//资源分配完毕,使用block原语进行自我阻塞
    {
        block(S-list);
    }
}
signal(semaphore *S)//每执行一次代表执行进程释放一个单位的资源
{
    S->value++;
    if(S->value>=0)//信号量链表中仍有等待资源的进程被阻塞,应调用wakeup原语唤醒
    {
        wakeup(S->list);
    }
}

值得注意的是,当value=1时,信号量转变为互斥信号量,用于进程互斥

5.3  AND型信号量

        AND型信号量的基本思想:将进程运行过程中的所有资源一次性全部分配给进程,待进程使用完后一并释放。从而避免了死锁。具体描述如下:

Swait(S1,S2,...,Sn)
{
    While(TRUE)
    {
        if (S1 >=1 && ... && Sn>=1 )
        {
          	for( i=1 ;i<=n; i++) Si--;
		    break;
	    }
        else
        {    
            Place the process in the waiting queue associated  with the first Si 
             found with Si < 1,and set the progress count of this process to the 
             beginning of Swait operation
        }
    }
}
Ssignal(S1,S2,...,Sn)
{
    while(TRUE)
    {    
	    for (i=1; i<=n; i++)
        {
            Si++ ;
            Remove all the process waiting in the queue associated with Si into 
            the ready queue                
        }
    }
}

对于Swait()操作:

        S1到Sn都表示所需资源,资源数都大于1,对每个资源进行--表示资源被占用,分配好资源之后跳出循环,wait操作结束。如果其中某个资源Si得不到满足,会执行else中的内容:把进程放进Si关联的阻塞队列中,然后程序计数器把指针移向wait操作开始。(wait操作是原语,遵循要执行都执行,执行不了就从头重新执行)

对于Signal操作:

        signal操作表示的是释放资源,把S1到Sn全部资源释放,并且把S1到Sn关联的阻塞队列全部置空,阻塞队列中的进程直接调度到就绪队列中。

猜你喜欢

转载自blog.csdn.net/aimat2020/article/details/121649441