Linux基础|多线程|条件变量——生产者和消费者模型

模型组成

  1. 生产者线程->若干
    • 生产商品或者任务放入到任务队列中
    • 任务队列满了就组塞,不满的时候就工作
    • 通过一个生产者的条件变量控制生产者线程组塞和非组塞
  2. 消费者线程->如果
    • 读任务队列,将任务或者数据读出
    • 任务队列中有数据就消费,没有数据就阻塞
    • 通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
  3. 队列->存储任务/数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存,可以是数组、链表,也可以使用stl容器:queue / stack / list / vector

场景描述:使用条件变量实现生产者和消费者模型,生产者5个,往链表头部添加结点,消费者也有5个,删除链表头部的结点

头文件

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

创建条件变量

pthread_cond_t cond;//创建条件变量
pthread_mutex_t mutex;//它要与互斥锁搭配使用

初始化条件变量

第一个变量是各自地址,第二个变量是各自的属性,一般为NULL

int main(){
	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init(&cond, NULL);
	return 0;
}

设置条件变量和互斥锁的资源释放

int main(){
	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init(&cond, NULL);
	
	pthread_mutex_destroy(&mutex);
	pthread_mutex_destroy(&cond);
	return 0;
}

创建线程和线程资源释放

int main(){
	pthread_t t1[5], t2[5];
	//生产者线程
	for (int i = 0; i < 5; ++i){
		pthread_create(&t1[i], NULl, producer, NULL);
	}
	//消费者线程
	for (int i = 0; i < 5; ++i){
		pthread_create(&t2[i], NULl, consumer, NULL);
	}
	
	//回收线程资源w w w w
	for(int i = 0; i < 5; ++i){
		pthread_join(t1[i], NULL); //第二个参数主要是拿pthread_exit()传递出的数据
		pthread_join(t2[i], NULL);
	}
}

创建生产者回调函数

//定义链表
strcuct Node{
	int number;
	struct Node* next;
};
//创建头结点
struct Node* head = NULL;

//生产者回调函数
void* producer(void* arg){
	while(1){
		//创建新结点
		struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
		//init node
		newNode->num = rand() % 1000;
		newNode->next = head;
		head = newNode;
		printf("生产者,ID: %ld, number: %d\n", pthread_self(), newNode->number);
		sleep(rand() % 3);
	}
	return NULL;
}

创建消费者回调函数

void* consumer(void* arg){
	while(1){
		struct Node* node = head;
		printf("消费者,ID: %ld, number: %d\n", pthread_self(), node->number);
		head = head->next;
		free(node);
		sleep(rand() % 3);
	}
	return NULL;
}

互斥锁的使用

  • 对生产者加锁
    多个线程不能同时往链表添加新结点,所以添加新结点的操作肯定是顺序执行的。所以在生产者回调函数部分,创建结点的临界区加上互斥锁。
while(1){
		//添加互斥锁
		pthread_mutex_lock(&mutex)
		//创建新结点
		struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
		//init node
		newNode->num = rand() % 1000;
		newNode->next = head;
		head = newNode;
		printf("生产者,ID: %ld, number: %d\n", pthread_self(), newNode->number);
		//解锁
		pthread_mutex_unlock(&mutex)
		sleep(rand() % 3);
}

这里要问自己一个问题:为什么要在这一块加锁?
该代码块,我们是创建一个新结点,然后再打印他的值,这里有一连串的变量操作。这块区域就是临界区,至于sleep()并不重要。

  • 对消费者加锁
	while(1){
		//加锁
		pthread_mutex_lock(&mutex)
		struct Node* node = head;
		printf("消费者,ID: %ld, number: %d\n", pthread_self(), node->number);
		head = head->next;
		free(node);
		//解锁
		pthread_mutex_unlock(&mutex)
		sleep(rand() % 3);
	}

条件变量的使用

尽管本例中,生产是没有上限的,但是消费是有上限的,我们需要在消费者的循环中看链表是不是已经空了,需要在循环中阻塞消费者线程

//消费者
	while(1){
		pthread_mutex_lock(&mutex)
		while(head == NULL){
			//阻塞当前的消费者线程
			pthread_cond_wait(&cond, &mutex);//条件变量的地址,互斥锁的地址
		}
		struct Node* node = head;
		printf("消费者,ID: %ld, number: %d\n", pthread_self(), node->number);
		head = head->next;
		free(node);
		pthread_mutex_unlock(&mutex)
		sleep(rand() % 3);
	}

当条件变量抢到互斥锁之后,他会自动开锁,所以所有被这把互斥锁阻塞的进程都能自由进出。

那么现在又有一个问题了,如果所有的消费者都阻塞到这里了,什么时候才能解除阻塞呢?
那就是链表中又有结点了,我们可以继续消费了。那么结点是生产者生产的,所以这个阻塞需要我们的生产者来进行唤醒

//生产者
while(1){
		pthread_mutex_lock(&mutex)
		struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
		newNode->num = rand() % 1000;
		newNode->next = head;
		head = newNode;
		printf("生产者,ID: %ld, number: %d\n", pthread_self(), newNode->number);
		pthread_mutex_unlock(&mutex);
		//生产出了产品唤醒消费者
		pthread_cond_broadcast(&cond)//变量就是条件变量的地址,这里也可以用pthread_cond_signal
		sleep(rand() % 3);
}

全代码

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

// 链表的节点
struct Node
{
    int number;
    struct Node* next;
};

// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;

// 生产者的回调函数
void* producer(void* arg)
{
    // 一直生产
    while(1)
    {
        pthread_mutex_lock(&mutex);
        // 创建一个链表的新节点
        struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
        // 节点初始化
        pnew->number = rand() % 1000;
        // 节点的连接, 添加到链表的头部, 新节点就新的头结点
        pnew->next = head;
        // head指针前移
        head = pnew;
        printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
        pthread_mutex_unlock(&mutex);

        // 生产了任务, 通知消费者消费
        pthread_cond_broadcast(&cond);

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void* consumer(void* arg)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
        // 一直消费, 删除链表中的一个节点
//        if(head == NULL)   // 这样写有bug
        while(head == NULL)
        {
            // 任务队列, 也就是链表中已经没有节点可以消费了
            // 消费者线程需要阻塞
            // 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
            // 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
            // 这函数会自动将线程拥有的锁解开
            pthread_cond_wait(&cond, &mutex);
            // 当消费者线程解除阻塞之后, 会自动将这把锁锁上
            // 这时候当前这个线程又重新拥有了这把互斥锁
        }
        // 取出链表的头结点, 将其删除
        struct Node* pnode = head;
        printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head  = pnode->next;
        free(pnode);
        pthread_mutex_unlock(&mutex);        

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化条件变量
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者, 5个消费者
    pthread_t ptid[5];
    pthread_t ctid[5];
    for(int i=0; i<5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    for(int i=0; i<5; ++i)
    {
        // 阻塞等待子线程退出
        pthread_join(ptid[i], NULL);
    }

    for(int i=0; i<5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    // 销毁条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);

    return 0;
}

作者: 苏丙榅
链接: https://subingwen.cn/linux/thread-sync/
来源: 爱编程的大丙

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值