生产者消费者模型

生产者消费者模型

1. 基本原理

在实现线程同步过程中,一个方法就是使用互斥锁 + 条件变量的方式。生产者消费者模型就是线程同步的典型例子。

其实现大概思路是这样的:

对于消费者来说,首先创建互斥锁并将其初始化;然后尝试去获取锁,因为只有拿到锁才可以去访问临界区的数据,但此时临界区不一定有数据啊,所以要先判断条件变量是否满足,如果不满足的话,那么消费者线程将阻塞在条件变量上等待条件变量满足(在这里即临界区有共享数据可以访问),等到条件变量满足了,那么消费者线程就会去访问数据(在这里埋一个坑,等到看代码时候说),在访问完共享数据之后,就会释放锁,给其他线程使用。

对于生产者来说,主要任务就是生产数据。等到数据生产好之后,就加锁将其放置到临界区中,放完之后立即释放锁,这样给其他线程,尤其是消费者线程消费临界区的机会。之后最重要的就是,通知,因为临界区有数据了啊,就可以通知消费者前来消费。其流程如下图所示:

2. 代码实现

直接看代码,几乎每一句我都给了注释,便于理解。这里我还想再提一个很重要的点:条件变量

这里主要讲述一个函数,即:

pthread_cond_wait();

这个函数其实有三个功能:

  1. 阻塞等待条件变量满足;

  2. 解锁!(刚刚加锁成功的互斥锁)

    注:执行到此行,对应下面代码中第36行,这里会把锁释放,也很好理解,因为既然执行到了这句,那么肯定临界区里面有问题,需要别的线程做一些工作让条件满足(这里就是生产者线程往里面添加结点),不然咱都一起阻塞着,不是耽误事情嘛,生产者生产之后,就会执行pthread_cond_signal()去通知其他线程临界区可以操作了。

  3. 当条件满足之后(即此函数返回),会重新加锁互斥量!

    注:这里当函数返回之后(即收到其他线程通知信号)重新加锁,因为,说明条件满足了,需要进行互斥访问临界区。

太神奇了,一个函数有三个功能,完全控制了互斥锁的加锁和解锁(其实,互斥量就是用来保护条件变量的,互斥量是一个小弟,用来保护条件变量大哥的,若大哥需要加锁我就加锁,需要我解锁我就解锁,老**了,哈哈哈)

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <error.h>

/* shared data */
typedef struct msg{
    int data;
    struct msg *next;
}Msg;

/* wrapped error func */
void err_(int ret, char *str){
    if(ret != 0){
        fprintf(stderr, "%s:%s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}

/* 定义共享数据的头结点 */
Msg *head;

/* 初始化互斥锁和条件变量,均为静态初始化,动态初始化调用 “ PTHREAD_MUTEX_INIT ” 和 “PTHREAD_COND_INIT” */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;

/* 消费者线程回调函数 */
void *consumer(void* arg){
    while(1){     //一直消费
    Msg* p1;      //初始化要消费的节点
    pthread_mutex_lock(&mutex);  //上锁,准备访问临界区数据
    while(head == NULL)       //!要是多个消费者,一定要用 while,单个消费者可以while或者if
                              // 其他消费线程不应该阻塞在锁上,而是都需要首先去判断条件变量是否满足,再去拿锁。
        pthread_cond_wait(&has_product, &mutex);   //若没有数据,则所有线程都将阻塞于条件变量上,等待生产者通知,以解除阻塞。

                    /* 多消费者执行逻辑:第一个线程被唤醒后,就去拿锁,然后访问临界区,这时候第二个线程阻塞在锁上,线程1访问结束后
                    释放锁,然后线程2拿到锁,但是发现临界区没有数据了,导致死锁。
                    所以要用while,保证每个被唤醒的线程在拿到锁之前都要去判断一下条件变量是否满足。   */

    p1 = head;
    head = p1->next;   //从头部移除第一个节点,相当于消费了一个节点

    printf("consumered id: %lu  %d\n",pthread_self(), p1->data);
    pthread_mutex_unlock(&mutex);   // 解锁
    free(p1);                       // 将消费掉的节点的内存空间free掉。
    sleep(rand() % 3);              // 留下一段时间,给生产者和其他线程执行的机会。
    }
    return NULL;
}

/* 生产者线程回调函数 */
void *producer(void* arg){
    while(1){    //一直生产 

    Msg *p2 = (Msg*)malloc(sizeof(Msg));   //生产一个节点
    if(p2 == NULL){
        printf("malloc failed!\n");     // 查看malloc是否成功
        exit(-1);
    }
    p2->data = rand() % 1000 + 1;       //给每个节点赋值
    printf("produced id: %lu  %d\n", pthread_self(), p2->data);

    pthread_mutex_lock(&mutex);          // 上锁,因为准备要将节点放到临界区中
                                        //注意,这里生产者是可以拿到锁🔒的,因为consumer线程里面调用pthread_cond_wait函数时候,就把互斥锁给释放了;
                                        //但是在pthread_cond_wait函数返回时,会尝试重新加锁,保证多个consumer之间的同步。

    p2->next = head;    // 头插法
    head = p2;
    pthread_mutex_unlock(&mutex);         // 解锁,保证锁的粒度越小越好
    pthread_cond_signal(&has_product);     //生产了说明临界区有数据了,通知其他线程去访问。

    sleep(rand() % 3);          // 留下一段时间,给生产者和其他线程执行的机会。
    }
    return NULL;
}

/* 主函数 */
int main(){
    pthread_t pid1, pid2;
    int ret;
    srand(time(NULL));   //将rand出来的值随机化,若没有的话,每次rand出来的一组值都是一样的

    ret = pthread_create(&pid1, NULL, producer, NULL);
    if(ret != 0)
        err_(ret, "pthread_create failed!\n");
    
    ret = pthread_create(&pid1, NULL, consumer, NULL);
    if(ret != 0)
        err_(ret, "pthread_create failed!\n");  

    pthread_join(pid1,NULL);             // 回收线程
    pthread_join(pid2,NULL);
    
    return 0;
}

最后注意的一点:如果想要多消费者或者多生产者的话,在main函数中多次调用pthread_create()即可,如果想知道这份数据是谁生产或者谁消费的,那么就可以调用pthread_self()函数,可以打印出该线程的ID。

1.https://www.bilibili.com/video/BV1KE411q7ee?p=179
2.Linux高性能服务器编程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值