Linux:多线程---同步生产者消费者模型
文章目录
- 1. 同步
- 1.1 同步与互斥的关系
- 1.2 条件变量
- 1.3 条件变量的接口
- 1.4 代码中易出问题的地方
- 1.5 条件变量的使用
- 2. 生产者消费者模型
- 2.1 生产者消费者模型的概念
- 序:在上一章中,我们深入了解了互斥的概念,浅谈了同步的概念,知道了线程安全的概念遗迹死锁及死锁的预防,而本章,我将深入同步,并带大家去深入生产者消费者模型。
1. 同步
1.1 同步与互斥的关系
调度问题和竞争问题导致的饥饿问题,就要用同步来处理,互斥是一种对于临界资源访问的保护,是一种解决方案,但是互斥只能在特定情况下才能发挥很好的作用,互斥具有局部性,而同步的方法,就能很好的填补这个空缺,互斥与同步不是两个对立的东西,而是互补的解决问题的方案。所以,同步问题是保证数据安全的情况下(互斥),让我们的线程访问资源具有一定的顺序性(同步)
1.2 条件变量
已经知道了什么是同步后,我们又该如何实现同步呢?—条件变量
还是以vip自习室为例:
对于这个自习室,每个同学来的时候一定是先来申请钥匙(锁资源)的,如果此时钥匙还没有人申请,则立马就能进入vip自习室进行自习,反之则进入等待队列的末尾中进行排队等待,而拿到钥匙的同学进入vip自习室出来后,需要先将钥匙释放后,敲响铃铛,铃铛响后,等待队列中的第一个队首的人就会离开等待队列,去申请钥匙,进入自习室。
其中的铃铛和等待队列就叫做条件变量,其中条件变量要包含简单的通知机制(铃铛),还要提供一个等待队列,能够让所有线程在等待队列中排列。
我们的库可以申请很多锁资源和条件变量,那么多锁资源就绪了,怎么知道是申请哪个锁资源,那么多条件变量就绪了,怎么知道是去哪个条件变量,所以,条件变量和锁都需要被管理,而要管理就离不开先描述,再组织,所以锁其实是一个结构体,方便管理,那么条件变量也一定是一个结构体,所以,对条件变量的管理就是对库中的结构体的管理。又因为能进入条件变量的等待队列进行等待的线程必定是先申请锁失败后,才进入等待队列进行等待的,所以条件变量就一定离不开互斥锁,或者说条件变量一定要配合互斥锁来使用。
总结:条件变量必须依赖于锁使用,让线程运行完后,进入一个等待队列中,由这个条件变量决定给谁锁(按照队列的先后顺序),依次执行。
1.3 条件变量的接口
pthread_cond_t:原生线程库提供的一个数据类型
pthread_cond_init:初始化一个条件变量,第一个参数cond填入要初始化的条件变量,第二个参数就是要填入的条件变量的属性,一般直接置为nullptr
pthread_cond_destroy:释放一个条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER就是定义一个全局的条件变量,和锁的定差不多一样的,不需要显式调用pthread_cond_destroy函数进行释放
怎么把线程放入等待队列进行等待?当然也是调用接口、
pthread_cond_wait:第一个参数传对应的条件变量,第二个参数传入对应的互斥锁
等待唤醒:
pthread_cond_broadcast函数:唤醒等待队列中的所有线程
pthread_cond_signal函数:唤醒队列中的第一个线程(唤醒一个线程)
1.4 代码中易出问题的地方
代码一如下:
#include<iostream>
#include<unistd.h>
#include<pthread.h>#define NUM 5void* handler(void *args)
{pthread_detach(pthread_self());uint64_t threadname = (uint64_t)args;std::cout<<"pthread:"<<threadname<<std::endl;
}int main()
{for(uint64_t i=0;i<NUM;i++){pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)i);}while(true) sleep(1);return 0;
}
代码一结果如图:
代码二如下:
#include<iostream>
#include<unistd.h>
#include<pthread.h>#define NUM 5void* handler(void *args)
{pthread_detach(pthread_self());uint64_t threadname = *(uint64_t*)args;std::cout<<"pthread:"<<threadname<<std::endl;
}int main()
{for(uint64_t i=0;i<NUM;i++){pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)&i);}while(true) sleep(1);return 0;
}
代码二结果如图:
观察代码一和代码二我们发现两个现象:
第一个是出现了相同的线程名,为什么第一个代码出现了,这是因为我们传i时候传的时候传的是i地址,而非i的参数,因为由于传的是i地址,所以当i发现改变,本质就是这个地址上的值发生了改变,从而导致线程的函数将改变后的i给赋了值,就会导致这样的情况,所以直接传地址就会导致这种数据上的错误,要注意容易出问题的地方。
第二个是打印在显示器上的内容错乱,这是因为打印在显示器上是在往显示器的文件中打印数据,而文件也是一种临界资源(共享资源),如果临界资源没有加锁(保护),就会出现同时访问,就容易出现打印在显示器上的信息是错乱的
1.5 条件变量的使用
演示代码如下:
#include<iostream>
#include<unistd.h>
#include<pthread.h>int cnt = 0;#define NUM 5pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void* handler(void *args)
{pthread_detach(pthread_self());uint64_t threadname = (uint64_t)args;std::cout<<"pthread:"<<threadname<<",create success!"<<std::endl;while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex); //? 为什么在这里?1.pthread_cond_wait让线程等待的时候,会自动释放锁std::cout<<"pthread:"<<threadname<<", cnt:"<<cnt++<<std::endl;pthread_mutex_unlock(&mutex);sleep(3);}
}int main()
{for(uint64_t i=0;i<NUM;i++){pthread_t tid;pthread_create(&tid,nullptr,handler,(void*)i);}sleep(3);std::cout<<"main thread ctrl begin: "<<std::endl;while(true){//唤醒在指定条件变量下的排队的线程pthread_cond_signal(&cond);std::cout<< "signal one thread..."<<std::endl;sleep(1);}return 0;
}
结果如图:
我们怎么知道我们要让哪一个线程去休眠?一定是临界资源不就绪,没错,临界资源也是有状态的!!!
你怎么知道临界资源是就绪还是不就绪?我们进行判断时,判断出来的!!!而判断临界资源也是访问临界资源,所以判断必须在加锁之后。
2. 生产者消费者模型
2.1 生产者消费者模型的概念
为什么有了超市,效率就高?假设没有超市,消费者就要找生产者拿所需的资源,而这也需要耗时,生产者生产也需要资源,导致消费者直接找生产者拿资源会很麻烦,但是如果生产者和消费者之间加了一个超市,就高效了呢?这是因为,生产者生产完资源后可以在超市中进行存储,消费者要资源的时候,就可以直接提供,不需要等了,这也符合为什么输入设备和输出设备之间为什么需要一个内存,一个道理。(超市—>大号的缓存)
厂家(生产者)关心的是超市(缓存)还有多少空位置,需要补多少货(资源),而顾客(消费者)关心的是超市(缓存)还有多少商品(资源),够不够买。生产和消费的行为,进行了一定程度上的解耦,生产者不需要考虑消费者,消费者不需要考虑生产者。
其中的生产者和消费者由线程承担,而超市就是特定结构的内存空间,而资源就是数据。本质是执行流在做通信。
其中超市可以支持忙闲不均,他是一个特定结构的内存空间(共享资源)—>会有并发问题
三种关系:
- 生产者vs生产者:互斥
- 消费者vs消费者:互斥
- 生产者vs消费者:互斥(保证数据安全),同步(生产和消费)
生产者消费者模型中:有3种关系,2种角色 — 生产和消费,1个交易场所 —特定结构的内存空间
优点:
- 支持忙闲不均
- 生产和消费进行解耦
总结:
本章深入同步,引出条件变量的概念,详细探讨了条件变量是什么,为什么和怎么用的三个重要问题,之后又对生产者消费者模型进行了一个大概的讲解。