Linux下消费者生产者模型的信号量版本和条件变量版本

本文解答了

  • 条件变量需要搭配锁的原因,
  • 使用while而不是if的原因,
  • pthread_cond_wait()函数中释放锁,等待pthread_cond_signal(&has_data)信号,
    加锁是原子操作的原因。

无论是哪种版本都是为了实现线程间的同步,以锁机制管理共享变量。
可以把规则当作:1。一个包子不可以两个人吃。1。一个客户吃到包子的必要条件是等待包子铺生产包子或者拿走包子铺已经生产的包子,包子铺生产包子的必要条件是有人来吃。

条件变量版本

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include<sys/mman.h>
#include<pthread.h>
void err_thread(int ret,char*str)
{
    if(ret!=0){
        fprintf(stderr,"%s:%s\n",str,strerror(ret));
        pthread_exit(NULL);
    }
}
struct msg{
    int num;
    struct msg *next;
};
struct msg *head=NULL;
/*struct msg是一个链表,每一个节点就是需要去共享的数据(包子),
这里初始化head为NULL,代表链表中没有数据(包子铺未生产)*/
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
//初始化锁,注意这里是全局变量
//初始化锁还可以通过在主函数 pthread_mutex_init(&mutex,NULL);
pthread_cond_t has_data=PTHREAD_COND_INITIALIZER;
//初始化条件变量,注意这里是全局变量
//初始化锁还可以通过在主函数  pthread_cond_init(&has_data,NULL);

void*consumer(void*arg)
{
    while(1){//循环消费
        struct msg *mp;
        pthread_mutex_lock(&mutex);//加锁
          /*
            条件变量需要搭配锁的原因:在线程中共享的数据都应该加上互斥锁,
            这个条件通常是多个线程的共享变量,对于共享变量很可能产生竞争,
            尤其还对共享变量加了条件限制,这个条件极有可能被一方改变。打
            个比方商铺做包子,1人网购,1人在店里买,同时去买。两个人都需
            要赶时间(1分钟),一个店员同意包子给店里的顾客,另一个店员
            却不知道,便跟网购的说1分钟内就给你这个包子。结果是店铺收了
            两份的钱,却只卖了一份包子,影响了顾客的体验,告诉两边顾客包
            子有了(注意这就是条件变量)(却没说只有1个!),事实上两边
            顾客等了同一段时间以后,一边顾客在得到包子以后,另一边的顾
            客的状态其实是得不到包子,必然会有人因此白等一段时间,不如先
            这个没得到包子的顾客有人订了这个包子,也就是加个锁,正当的
            做法是在做一个包子上作上一个标签,指明这个包子有人买了。那
            么这个顾客就可以先不去等(wait),他也就在pthread_mutex_lock(&mutex)
            停在这句话了。
          */
        
		   while(head==NULL){    
		   /*使用while(head==NULL)而不是if的原因:
		consumer线程1条件不满足,进入while循环,进入  pthread_cond_wait等待,
		其中释放锁,
		consumer线程2拿到锁,并且head还是NULL,进入while循环,进入pthread_cond_wait等待
		,其中释放锁.
		当produser在释放信号来结束consumer线程等待的时候,线程1,2苏醒,争夺锁,假
		如1先拿到锁,打印数据,更改条件使得head==NULL,释放锁,2再拿到锁,打印数
		据,但其实这样2就仍然是在head==NULL的条件下继续后面的事情,这不符合原有的
		逻辑。这是因为这里1存在对cond的pv操作。你想一个商家说我生产好包子啦,第一个
		抢到包子,可以吃了;第二个人在没有包子了的情况下是不是要(while)继续等下一个包
		子呢? */
            printf("顾客:我在等\n");
            pthread_cond_wait(&has_data,&mutex);//等待

            /*
            pthread_cond_wait中释放锁,等待pthread_cond_signal(&has_data)信号,
            加锁是原子操作的原因:如果在释放锁和等待pthread_cond_signal(&has_data)信号
            这两个函数之间有一定时间差,刚好在没有进入pthread_cond_signal(&has_data)函数
            的时候pthread_cond_signal(&has_data)信号来了,
            这使得这个过程错过了信号,这不合理。
            因此把他结合在一起成为原子操作是最为合适的。
            在等待pthread_cond_signal(&has_data)信号和加锁的过程也同理
            */
        }
        mp=head;
        head=mp->next;
        pthread_mutex_unlock(&mutex); //释放锁
        printf("consumer id:%ld,get :%d\n",pthread_self(),mp->num);//打印数据
        free(mp);
        sleep(rand()%3);  
    }
    return NULL;
}
void*produser(void*arg)
{
    while (1){////循环生产
        struct msg*mp=malloc(sizeof(struct msg));
        mp->num=rand()%1000+1;
        printf("produce %d\n",mp->num);
        pthread_mutex_lock(&mutex);
        mp->next=head;//将消费品(包子)放到链表头(柜台)
        head=mp;
        pthread_mutex_unlock(&mutex);//解锁
        pthread_cond_signal(&has_data);//发送完成信号
        sleep(rand()%3); 
    }
    return NULL;   
}
int main()
{
    srand(time(NULL));
    int ret;
    pthread_t pid,cid;
    ret=pthread_create(&pid,NULL,produser,NULL);//建生产者线程
    if(ret!=0)
        err_thread(ret,"pthread_create produser error ");
    for (int i = 0; i < 3; i++){//
        ret=pthread_create(&pid,NULL,consumer,NULL);//建3个消费者线程
        if(ret!=0)
            err_thread(ret,"pthread_create cosumer error ");
    }
        pthread_join(pid,NULL);//回收线程资源
        pthread_join(cid,NULL);//回收线程资源
    return 0;
}

结果:

顾客:我在等
顾客:我在等
produce 574
顾客:我在等
consumer id:140558006253312,get :574
produce 974
consumer id:140557905032960,get :974
顾客:我在等
顾客:我在等
produce 40
consumer id:140557997860608,get :40
produce 514
produce 426
consumer id:140558006253312,get :426
consumer id:140557905032960,get :514
顾客:我在等
produce 63
consumer id:140557905032960,get :63
produce 231
consumer id:140557905032960,get :231
顾客:我在等
顾客:我在等
produce 202
consumer id:140557997860608,get :202
顾客:我在等

是的,挺繁琐的。

信号量版本

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include<sys/mman.h>
#include<pthread.h>
#include<semaphore.h>
#define NUM 5
int queue[NUM];
sem_t blank_number,product_number;//生产限额和产品的数量void*producer(void*arg)
{
    int i=0;
    while(1){//循环生产
        sem_wait(&blank_number);//总位置数--,柜台少了一个位置,0则阻塞
        queue[i]=rand()%1000+1;
        printf("---produce ---%d\n",queue[i]);
        sem_post(&product_number);//多个一个产品
        i=(i+1)%NUM;//循环队列
        sleep(rand()%1);
    }
}
int j=0;//注意这里把j放在全局,三个子进程共享便可以实现正确的循环着消费的目的
void *consumer(void*arg)
{
    while(1){
    sem_wait(&product_number);//总产品数--,0则阻塞
    printf("consumer:%d\n",queue[j]);
    queue[j]=0;
    sem_post(&blank_number);//柜台多了一个位置
    j=(j+1)%NUM;//循环队列
    sleep(rand()%10);
    }
}
int main()
{
    pthread_t pid,cid;
    sem_init(&product_number,0,0);//初始化0个产品
    sem_init(&blank_number,0,NUM);/*初始化NUM个位置,你生产再快,
    (柜台)最多放NUM 个包子*/
    pthread_create(&pid,NULL,producer,NULL);//创建生产者线程
   	for(int i=0;i<3;i++)
	    pthread_create(&cid,NULL,consumer,NULL);//创建3个消费者线程
    pthread_join(pid,NULL);//回收线程资源
    pthread_join(cid,NULL);//回收线程资源
    sem_destroy(&product_number);//销毁信号量
    sem_destroy(&blank_number);//销毁信号量

}

结果:

---produce ---384
consumer:384
---produce ---916
consumer:916
---produce ---387
consumer:387
---produce ---422
---produce ---28
---produce ---60
---produce ---927
---produce ---427
consumer:422
---produce ---212
consumer:28
---produce ---430
consumer:60
---produce ---863
consumer:927
---produce ---136
consumer:427
---produce ---23
consumer:212
---produce ---168
consumer:430
---produce ---12

信号量版本在多个线程间共享,相比条件变量版本更加简洁

发布了14 篇原创文章 · 获赞 3 · 访问量 538

猜你喜欢

转载自blog.csdn.net/adlatereturn/article/details/104721644
今日推荐