【1. 问题描述:】
生产者消费者问题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。 如下图所示:
【2. 关键点:】
消费者和生产者必须互斥的使用缓冲区。缓冲区空时,消费者不能读取数据。缓冲区满时,生产者不能添加数据。消费者和生产者是一种同步关系,即:在生产者生产时,消费者不能消费;消费者消费时,生产者不能生产;只有生产者生产之后,消费者才能消费。首先我们要明确一个点:我们需要循环从缓冲区放/取数据,缓冲区类似一个循环数组,即:
生产者往数组中放数据,(i+1)%N的放,这样数组不会出现溢出情况。消费者从数组中取数据,(j+1)%N的取,这样不会出现数组越界。我们假设缓冲区现在大小为5,如果我们不进行取余运算,那么生产者取数据时,j=6就会越界,但此时缓冲区中还有数据在1号为放着,所以应该取余6%5=1,到1号位去取数据。
其次,我们根据关键点确定我们需要控制三件事情:
缓冲区互斥访问,我们可以用互斥锁,信号量来控制这两个线程对临界区的访问,因为只有1块临界资源,所以初始值为1。缓冲区所有格子放满了时,生产者不能生产。我们可以记录当前缓冲区没放数据的格子数,即空格子数,那么每次生产者往缓冲区放数据时,空格子数量-1,消费者取数据时,数量+1。当空格子数==0时, 生产者-1会阻塞。故我们可以用信号量来表示缓冲区为空状态的控制,初始值为N,还没有往里面放数据,生产P(-1),消费(V+1),当P(-1)操作阻塞,表示缓冲区的所有格子都放满了。缓冲区中没有数据时,消费者不能消费,同理也用信号量来控制,我们记录当前放数据的格子的个数,初始值为0, 表示此时没有数据,生产者生产+1,消费者消费-1,当==0时,消费者阻塞。假设我们定义三个控制变量分别为:mutex缓冲区信号量;empty缓冲区空格子数;full缓冲区填充数据的格子数。缓冲区用数组表示,大小为5,假设现在已经放了两个数据,那么有下图:
最后我们构思生产者消费者的整体流程,都是循环的读,写数据的,buff数组为临界资源。
生产者:
P(empty) 操作,获取空格子数,此时empty-1,P(mutex)获取临界资源访问权,如果成功mutex=0,失败表示消费者正在读,阻塞mutex=-1。给buff[i]放入随机数,i=(i+1)%N,下一个放数据的位置。V(full),full+1表示有数据格子数增加,V(mutex) 释放临界资源消费者:
P(full) 操作,读取数据,此时full-1,表示有数据格子数减一;P(mutex) 获取临界资源访问权,mutex=0。从buff[j]中取数据,j=(j+1)%NV(empty),表示空格子数加一,V(mutex) 释放临界资源我们开辟2个生产者,2个消费者,对缓冲区大小为5的buff进行写数据,取数据。
# include<stdio.h> # include<assert.h> # include<unistd.h> # include<stdlib.h> # include<pthread.h> # include<semaphore.h> # include<string.h> int buff[5]={0};//缓冲区 sem_t mutex;//控制缓冲区 sem_t empty;//控制空格子数 sem_t full;//控制有数据格子 int i=0; int j=0; void* producer(void* argc) { while(1) { int index=(int)(argc); srand(time(NULL));//生成随机数字 int data=rand()%11; sem_wait(&empty); sem_wait(&mutex); buff[i]=data; printf("生产者线程%d往buff%d下标添加数字%d\n",index,i,data); i=(i+1)%5; sem_post(&full); sem_post(&mutex); sleep(1); } } void* consumer(void* argc) { while(1) { int index=(int)(argc); srand(time(NULL)); int data=rand()%11; sem_wait(&full); sem_wait(&mutex); printf("消费者线程%d从buff第%d取出数字%d\n",index,j,buff[j]); buff[j]=0; j=(j+1)%5; sem_post(&empty); sem_post(&mutex); sleep(1); } } int main() { pthread_t id[4]; sem_init(&mutex,0,1); sem_init(&empty,0,5); sem_init(&full,0,0); int i=0; for(;i<2;i++)//创建生产者 { pthread_create(&id[i],NULL,producer,(void*)(i+1)); } for(;i<4;i++)//创建消费者 { pthread_create(&id[i],NULL,consumer,(void*)(i+1)); } pthread_join(id[0],NULL); pthread_join(id[1],NULL); pthread_join(id[2],NULL); pthread_join(id[3],NULL); sem_destroy(&mutex); sem_destroy(&empty); sem_destroy(&full); exit(0); }1. 解耦 多了一个缓冲区,把生产者和消费者之间的强耦合解开,变成了生产者和缓冲区,消费者和缓冲区之间的弱耦合。 2. 支持并发 生产者和消费者可以是两个独立的并发主体。生产者把制造出来的数据添加到缓冲区,就可以再去生产下一个数据了。而消费者也是一样的,从缓冲区中读取数据,不需要等待生产者。这样,生产者和消费者就可以并发的执行。 3. 支持忙闲不均 生产者只需要将生产的数据添加到缓冲区,缓冲区满了就不生产了。消费者从缓冲区中读取数据,缓冲区空了就不消费了,使得生产者/消费者的处理能力达到一个动态的平衡。
【1. 问题描述:】 有读者和写者两个并发线程,共享一个文件,当两个或两个以上的读线程同时访问共享数据时不会产生副作用,但若某个写线程和其他线程(读/写)同时访问共享数据时则导致数据不一致的错误,因此要求:(1)允许多个读者可以同时对文件执行读操作(2)只允许一个写者往文件中些信息。(3)任一写者在完成写操作之前不允许其他读者或写者工作。
可以概括为下图: 【2. 关键点:】
允许多个读者访问临界资源,不是互斥关系。读者-写者互斥,写者-写者互斥,所以每次进入临界区前都需要进行控制,不能允许其他线程再进入。我们可以使用读写锁实现,那个很简单,只需要调用读写锁即可,主要学习如何用信号量解决这个问题。如何实现读者进入临界区,写者进不去,但其他读者可以进去,这时读者写者问题的核心。首先,我们要知道这块临界区域的数据是不会被删除的,即读者读了数据之后不会像消费者那样,将数据清0。因为多个读者可以同时读,如果删了数据,其他读者就读不到了。故缓冲区的数据不存在删除问题,那我们就可以采取一个变量来表示缓冲区,写者不断累加变量,读者读取变量的值即可。
其次,根据关键点分析出来需要控制3件事:
读者-写者,写者-写者互斥问题很好处理,我们只需要设定信号量,初值为1,读者/写者进入临界区前P操作获取资源,此时P=0,其他读/写再P会阻塞,出临界区后V操作释放资源。关键是如何控制读者-读者,即读者进入临界区后,读者还可以进入,写者不能进入。我们从两个角度分析: 互斥角度: 写者不能进入的原因是:读者P操作获取临界资源,此时P=0,再次P操作写者就会阻塞,实现了读者-写者互斥。现在要让读者不会阻塞,可以设置第二次进来的读者不进行P操作,那就不会产生阻塞了。共享角度: 第一个进来的读者必须要P操作获取临界资源,第二次进来的读者无需P操作;那也就无需V操作,最后一个读者离开时就必须V操作释放临界资源。 故我们需要用一个变量count来记录读者的个数,当count==0表示第一个读者进入,需要P操作获取资源,其他读者进入直接读取数据,count++即可;读取数据过后,count–,如果count==0表示最后一个读者,需要V操作释放资源。 引入count计数后,在同一时刻只能有一个线程对count进行++或- -操作,否则就会出现统计读者个数不唯一的问题,所以我们也要对count临界资源进行控制,实现同步,即定义信号量,初值为1,读者进入临界区P获取,++后,V释放;读者读取完数据,出临界区前P获取,- -操作后,V释放。总结就是:定义count计数,统计读者个数;定义信号量rw对临界资源进行控制;定义mutex对count临界资源进行控制。
最后,我们可以根据分析构思出读者、写者的流程:(这种办法是读者优先)
读者
P(mutex) 获取count临界资源,判断count==0,成立表示是第一个读者,那么就获取临界区资源P(rw),不等于0,表示是第2、3……读者进入临界区,**count++**即可,增加读者个数,V(mutex) 释放count临界资源。读取数据,打印数据。对数据处理完成后,P(mutex) 获取count临界资源,判断count==0,成立表示最后一个读者结束,那么就释放临界区资源V(rw),不等于0,表示是第2,3……读者离开临界区,count- -即可,减少读者个数,V(mutex) 释放count临界资源。写者
P(rw)获取临界区资源,写数据,V(rw)释放临界区资源。我们前面说过可以用读写锁实现,也可以用信号量实现,我们将两种代码都实现一下。都创建3个读者,编号为1、2、3;2个写者,编号为4、5。对全局变量data进行操作,写者每次data++,读者每次读data。
定义一个读写锁,读者进去加读锁,写者进去加写锁即可。
# include<stdio.h> # include<stdlib.h> # include<unistd.h> # include<assert.h> # include<string.h> # include<pthread.h> int data=0; pthread_rwlock_t rw; void* reader(void* arg) { int index=(int)(arg); while(1) { pthread_rwlock_rdlock(&rw);//加读锁 printf("---start read\n"); printf("%d号读者,读出数据data=%d\n",index,data); printf("---end read\n"); pthread_rwlock_unlock(&rw); sleep(1); } } void* writer(void* arg) { int index=(int)(arg); while(1) { pthread_rwlock_wrlock(&rw);//加写锁 printf("---start write\n"); data++; printf("%d号写者,写入数据data=%d\n",index,data); printf("---end write\n"); pthread_rwlock_unlock(&rw); sleep(1); } } int main() { pthread_rwlock_init(&rw,NULL); pthread_t id[5]; int i=0; int res; for(;i<3;i++) { res=pthread_create(&id[i],NULL,reader,(void*)(i+1)); assert(res==0); } for(;i<5;i++) { res=pthread_create(&id[i],NULL,writer,(void*)(i+1)); } i=0; for(;i<5;i++) { pthread_join(id[i],NULL); } pthread_rwlock_destroy(&rw); exit(0); }如果我们不加锁进行控制,那么就会出现下面的结果:
我们加上锁,就是我们展示的代码,那么运行结果为:
按照我们分析的结果定义即可,
# include<stdio.h> # include<stdlib.h> # include<unistd.h> # include<assert.h> # include<string.h> # include<pthread.h> # include<semaphore.h> int data=0;//临界资源 int count=0;//计数器,记录多少个读者 sem_t rw;//控制临界资源 sem_t mutex;//控制计数器 void* reader(void* arg) { int index=(int)(arg); while(1) { sem_wait(&mutex);//获取计数器 if(count==0)//第一个读者 { sem_wait(&rw);//给临界资源加锁 printf("first reader lock rw\n"); } count++;//后面的读者不用加锁 sem_post(&mutex);//释放计数器 printf("%d号读者,读出数据data=%d\n",index,data); sem_wait(&mutex); count--; if(count==0)//最后一个读者解锁 { sem_post(&rw); printf("last reader unlock rw\n"); } sem_post(&mutex); sleep(1); } } void* writer(void* arg) { int index=(int)(arg); while(1) { sem_wait(&rw); data++; printf("%d号写者,写入数据data=%d\n",index,data); sem_post(&rw); sleep(1); } } int main() { sem_init(&rw,0,1); sem_init(&mutex,0,1); pthread_t id[5]; int i=0; int res; for(;i<3;i++) { res=pthread_create(&id[i],NULL,reader,(void*)(i+1)); assert(res==0); } for(;i<5;i++) { res=pthread_create(&id[i],NULL,writer,(void*)(i+1)); } i=0; for(;i<5;i++) { pthread_join(id[i],NULL); } sem_destroy(&rw); sem_destroy(&mutex); exit(0); }可以明显的看到实现了读者写者的所有要求:读-读共享,读-写互斥,写-写互斥。
上面对读者写者的解决方案存在着潜在问题:只要有读线程还在读,写线程就要一直阻塞等待,会出现“写者饥饿”问题。如读者1执行了P(rw)获取了临界资源,这时写者P就会阻塞,但是再来一个读者,会继续读,那么就会出现读者一直读,写者一直阻塞。
我们可以通过再加一个写优先信号量W,来实现写者优先或实现公平读写。有兴趣的可以深入了解一下。
【1. 问题描述:】
一张圆桌上坐着 5 名哲学家,桌子上每两个哲学家之间摆了一根筷子,桌子的中间是一碗米饭,如图所示: 哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿的时候,才试图拿起左、右两根筷子(一根一根拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考。
【2. 关键点:】
5个哲学家与左右邻居对其中间筷子的访问是互斥关系,故5根筷子都是互斥量。只有拥有两根筷子的哲学家才可以吃饭。要保证哲学家拿到左右两个筷子而不造成死锁,每个哲学家都有机会可以吃饭,不会造成饥饿现象。根据上述问题描述我们可以写出一段伪代码来表示哲学家就餐的过程:
sem chopstick[5]={1,1,1,1,1}//5根筷子 Pi() { while(1) { P(chopstick[i]);//取左边筷子 P(chopstick[(i+1)%5]);//取右边筷子 eat;//吃饭 V(chopstick[i]);//放左边筷子 V(chopstick[(i+1)%5]);//放右边筷子 think;//思考 } }这是根据题目可以得到的思路,存在很大的问题,如当五个哲学家都想要进餐时,分别拿起它们左边的筷子,此时筷子已经拿完了,等到他们再想拿右边的筷子的时候,就全被阻塞了,这时谁不肯放下自己手中的那一根筷子,阻塞就无法解决,除非介入外力,否则5个哲学家都吃不上饭,这就是出现了死锁。
那我们现在就必须要防止死锁的发生,根据死锁的预防,主要有三种办法可以解决:
最多允许四个哲学家同时进餐。仅当一个哲学家左右两边的筷子都可用时才允许他抓起筷子,破坏请求条件。对哲学家顺序编号,要求奇数号哲学家先抓左边的筷子,然后再抓他右边的筷子,而偶数号哲学家刚好相反,破坏循环等待。现在有5个哲学家,5根筷子。这种方案的思想就是: 我们每次只让4个哲学家进行筷子的争夺,即4个哲学家,5根筷子,总会有一个哲学家拿到两根筷子吃饭,吃完后再放下筷子,这样就可以保证每次都有一个哲学家可以吃饭。
那我们就需要对下列资源进行控制:
筷子资源,我们就用信号量来保存,5个信号量即可,初始化为1。要控制四个哲学家,那么就需要再定义一个信号量来控制,我们可以定义count,初始值为4,每个哲学家在拿筷子之前先P(count) 表示它已经进来了,这时count- -,直到count==0时,表示此时已经有4个哲学家了,如果最后一个哲学家P(count)会被阻塞,把筷子放好后,V(count) 表示它出去了。里面的哲学家就可以开始拿筷子了,先拿左再拿右,吃完放回去。根据描述我们写出伪代码:
sem cho[5]={1,1,1,1,1}//5根筷子 sem count = 4;//设置一个count最多有四个哲学家可以进来 void pi(int i) { while(1) { P(count);//请求进入房间进餐,获取资源,当count为0的时候,不能允许哲学家再进来了,阻塞 P(cho[i]);//请求左手边的筷子 P(cho[(i+1)%5]);//请求右手边的筷子 eat(); V(cho[i]); //释放左手边的筷子 V(cho[(i+1)%5]);//释放右手边的筷子 V(count);//退出房间释放资源 } }这种方案的思想是一种破坏请求条件的预防,即将资源一次性分配,一次性分配所有资源,这样就不会再有请求了。这种解决办法就是一次性分配两个筷子,不允许其他哲学家打断分配两根筷子的这个过程,这样可以保证至少有2个哲学家分别获得2根筷子,进行吃饭。
那我们就需要对下列资源进行控制:
筷子资源,信号量表示。因为要一次性分配两根筷子,不被其他人打断,那么就需要再定义一个信号量mutex,去控制分筷子这个状态,每一个哲学家在分筷子前,需要获取分筷子状态P(mutex),此时其他哲学家就不能抢夺筷子,之后再分配左边,分配右边筷子,分配完成后就释放V(mutex)分筷子状态,此时其他哲学家可以进行分筷子状态的获取。得到2根筷子的哲学家就可以开始吃饭,吃完了释放筷子即可。我们可以写出伪代码:
sem cho[5]={1,1,1,1,1}//5根筷子 sem mutex=1;//控制分筷子状态 void pi(int i) { while(1) { P(mutex);//获取分筷子状态,同一时刻只允许一个哲学家获取 P(cho[i]);//请求左手边的筷子 P(cho[(i+1)%5]);//请求右手边的筷子 V(mutex);//释放资源,表明分配筷子完毕 eat(); V(cho[i]); //释放左手边的筷子 V(cho[(i+1)%5]);//释放右手边的筷子 } }我们尝试实现一下这份伪代码,那么代码如下图所示:
# include<stdio.h> # include<stdlib.h> # include<unistd.h> # include<assert.h> # include<string.h> # include<pthread.h> # include<semaphore.h> sem_t sem[5];//信号量数组,控制5根筷子 sem_t mutex;//控制是否进入分筷子状态 void* eat(void* arg) { int i=(int)(arg); // while(1)加上循环表示5位哲学家不断地需要进行吃饭,不加就是一轮吃饭 // { sem_wait(&mutex);//进入分筷子状态 sem_wait(&sem[i]);//分左边筷子 sem_wait(&sem[(i+1)%5]);//分右边 sem_post(&mutex);//分筷子结束,退出状态 printf("哲学家%d在吃饭\n",i+1);//吃饭饭 sem_post(&sem[i]);//放下筷子 sem_post(&sem[(i+1)%5]); // } } int main() { int i=0; for(;i<5;i++) { sem_init(&sem[i],0,1);//初始化信号量数组 } sem_init(&mutex,0,1); pthread_t id[5]; i=0; for(;i<5;i++) { pthread_create(&id[i],NULL,eat,(void*)(i));//创建5个哲学家 } i=0; for(;i<5;i++) { pthread_join(id[i],NULL); } i=0; for(;i<5;i++) { sem_destroy(&sem[i]); } sem_destroy(&mutex); exit(0); }成功解决了死锁问题。
首先对筷子编号为:1、2、3、4、5,对哲学家顺序编号,为:1,2,3,4,5。规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家相反,先去拿他右边的再去拿他左边的。那么我们可以画出下图: 可以看到哲学家都在争夺筷子,这种办法不会造成死锁,因为总有一个哲学家争夺成功,获得筷子,然后再去争夺另一根,最后总会有一个哲学家获得两个筷子可以吃饭。
写出伪代码:
sem cho[5]={1,1,1,1,1}//5根筷子 void pi(int i) { while(1) { if(i%2==0)//偶数哲学家,先右后左 { P(cho[(i+1)%5]);//请求右手边的筷子 P(cho[i]);//请求左手边的筷子 eat(); V(cho[(i+1)%5]);//释放右手边的筷子 V(cho[i]); //释放左手边的筷子 } else//奇数哲学家,先左后右 { P(cho[i]);//请求左手边的筷子 P(cho[(i+1)%5]);//请求右手边的筷子 eat(); V(cho[i]); //释放左手边的筷子 V(cho[(i+1)%5]);//释放右手边的筷子 } } }加油哦!💪。