上篇文章介绍完线程的概念后,我们将在这篇文章中初步探讨线程编程以及线程应用中的问题,这篇文章将以抢票系统为例,贯穿整篇文章。笔者将介绍在多线程编程中会出现的问题,什么是同步?什么是互斥?为什么多线程编程常有加锁的概念,什么又是生产者和消费者模型,读完这篇文章,你会得到相应的答案,笔者这里强烈建议各位把文中给出的demo示例自己实现一遍
多线程这部分必须要理论和实操相结合,并不像前面虚拟地址空间,页表这些理论性的知识,只要理解就可以了
目录
初步应用多线程
互斥
锁
深入理解锁
封装锁
死锁
避免死锁的方法
线程同步
生产者与消费者模型
条件变量
条件变量的相应接口
条件变量代码实操
理解条件变量
基于BlockingQueue的生产者消费者模型
生产者消费者模型的意义
初步应用多线程
多线程是一朵带刺的玫瑰,在享受它带来方便的同时要煞费思量多线程带来的问题
接下来咱们以一个实例来刨析使用线程究竟会带来哪些问题,咱们模拟一个抢票的场景,在main函数里创建一个次线程,然后主线程和次线程同时进行抢票,代码如下
#include<pthread.h>
#include<iostream>
#include<unistd.h>
int counter = 100;
void* start_routine(void* args)
{
while(counter > 0){
counter--;
std::cout << "抢票用户:次线程,剩余票数: " << counter << std::endl;
}
return nullptr;
}
int main()
{
pthread_t thread;
pthread_create(&thread, NULL, start_routine, NULL);
while ( counter > 0){
counter--;
std::cout << "抢票用户:主线程, 剩余票数:" << counter << std::endl;
}
pthread_join(thread, NULL);
return 0;
}
代码很简单,看运行结果
可以发现,所有的票都是主线程抢到的,次线程一张票都没有抢到,这是因为次线程还没有创建完的时候,CPU不断执行主线程的循环,把票抢完了
我们要模拟出多个线程间交叉执行的情况,而不是某一个线程把所有的票都抢完了,想要模拟出这个场景,我们就得人为让CPU频繁的调度
所以,我们需要在抢票循环之前加上sleep,当某个线程准备开抢时,先休眠一会,被OS挂起,给其他线程启动的机会。但是我们不敢直接用sleep,因为该函数是以秒为单位的,以CPU的速度,如果休眠1秒,会在这个时间间隙内被其中一个线程全部抢完,所以我们要使用另一个函数——usleep
该函数和sleep一样,不过是以微妙为单位,我们调整参数到毫秒级别,这样两个线程就能够交叉抢票了,如下是修改后的代码
#include<pthread.h>
#include<iostream>
#include<cstdio>
int counter = 100;
void* start_routine(void* args)
{
while(true){
if (counter > 0){
counter--;
usleep(1234);
std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
}
else break;
}
return nullptr;
}
int main()
{
pthread_t thread;
char second_thread[] = "次线程";
char main_thread[] = "主线程";
pthread_create(&thread, NULL, start_routine, (void*)second_thread);
start_routine((void*)main_thread);
pthread_join(thread, NULL);
return 0;
}
如下图是运行结果
根据图上信息,可以发现,两个线程确实实现了交叉抢票,但是吧,最后剩余票数出现了-1是什么情况,两个线程正常抢票,都是进行counter > 0才能进入抢票,可为什么就出现了负数了呢?接下来刨析这个问题(再次建议读者完成代码编写,亲自感受这个过程)
互斥
当票数counter只剩1的时候,此时次线程从内存中读取数据,发现counter为1,然后判断counter>0为真,但是一进入循环内,就遇到了usleep,于是次线程会被OS挂起,然后OS唤醒主线程,主线程去内存中加载counter数据时,发现counter的值仍为1,就通过了判断语句,同样的它将遇到usleep,被挂起。之后OS唤醒次线程,次线程开始执行counter-1,然后循环,判断counter为0,就退出了循环,接着主线程被OS唤醒,开始执行counter-1,注意此时的counter已经为0了,减去1之后就是-1,这也是为什么最后主线程的剩余票数会变为-1的原因
可以发现,造成这个问题的根本原因就是因为抢票过程不具备原子性,什么是原子性?当一件事具备了原子性,就说明这件事要么做成,要么失败,没有中间状态可言。回顾一下刚才抢票时,次线程检测到counter为1,它进入了循环,如果它具备原子性,那么对于抢票这件事,要么它抢票成功,counter减为0,要么它抢票失败,counter仍为1,轮到主线程抢票,在其它线程眼里这个过程是一瞬间,没任何中间过程的
但事实是它是有中间状态的,它不可能在物理上的一瞬间就抢到票把counter减为0,它是要逐步执行代码的,执行代码是需要时间的,就单单counter--这个操作,就至少得翻译成三条汇编语句逐条执行,在这段时间内,counter的值仍为1,其它线程就有机会进来了
除了上面出现负票这种情况,还有可能会导致票越抢越多的情况,只不过这种情况很难模拟出来,但它确实存在,故此我们理论上分析出现这种情况的原因
假设现在有100张票,有A,B两个进程一起抢票
这看似一个简单的counter-1操作,背后的CPU至少要分成这三部分完成,上面就是正常抢票过程中,CPU所执行的操作
如上图,因为A被挂起,长时间处于中间状态,B在这段时间内将票抢空,而A被唤醒后,继续执行中断前的操作,导致了票明明被B抢空了,却又变回99这种越抢越多的情况
面对类似抢票这种场景就得保证多个线程在执行任务时要是原子的,互斥是保证原子性的手段之一,正在抢的那个线程要么抢成功,要么抢失败,而在它抢的过程中,固有的中间状态靠互斥性来解决,也就是其他线程与当前正抢票的线程保持互斥,不能打扰它抢票
如何做到互斥?解决方法就是加锁
锁
什么是锁呢?前面提到过,抢票时我们需要对票这种共享资源进行保护,也就是抢票这个事件是要原子的,但是就单单counter--这种操作,得有三条汇编指令才能实现,这就产生了中间状态,锁就是来锁住这个中间状态,大致流程如下
当线程A正在抢票时,它需要申请一把锁,只要它申请锁成功,那么它就可以放心去执行它的抢票代码,其它的线程要想抢票也得先申请锁,但是锁已经被A拿走了,它们只能阻塞等待A释放锁才能够申请到锁
上述过程中,锁将线程A抢票的中间状态锁住了,就能够实现抢票的原子性,在其它线程看来,A抢票就是原子的,就是一瞬间,没有中间过程的。因为A拿到锁正抢票时,其它线程到了就得阻塞等待从而被OS挂起,当它们被唤醒的时候,说明A已抢完票,释放了锁,在它们眼里这可不就是一瞬间的事嘛
明白锁的作用,接下来看看怎么使用锁,锁本质也是一种数据类型,其类型为:pthread_mutex_t,使用锁之前得先申请一个锁
申请一个锁:pthread_mutex_t mutex;
申请完锁需要对其进行初始化,使用初始化接口:
pthread_mutex_init( pthread_mutex_t *mutex );
第二种初始化方法:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
初始化完成之后,就可以使用锁了,使用锁分为加锁和解锁
加锁的接口为:pthread_mutex_lock( pthread_mutex_t *mutex);
解锁的接口为:pthread_mutex_unlock( pthread_mutex_t *mutex);
还有一个加锁的接口为:pthread_mutex_trylock( pthread_mutex_t *mutex);
那这两个加锁接口有什么区别呢?举个例子
有线程A,B共同使用一把锁,当使用pthread_mutex_lock加锁时,A加锁完,B再来就得阻塞等待。而使用pthread_mutex_trylock加锁时,A加锁完,B再来并不会阻塞等待,而是报错返回,因此pthread_mutex_trylock也被称为非阻塞式加锁
那在哪里使用锁呢?在main函数开头就加锁,return前解锁吗?当然不是,我们要明白加锁锁的是一种中间状态,把抢票这个具有过程性的操作给保障成原子性。原本有A,B两个线程同时并行执行,但在对公共资源进行争夺时,就会带来很多的麻烦,采用加锁相当于把这些线程在抢夺资源时排好队,谁先来的谁开始,后面的排队等待,变成一种串行的执行方式
串行排队等待,程序的效率必然会下降,因此在满足公共资源保护的前提下,加锁的范围要尽量的小,接下来通过demo来演示加锁的用法,还是用上面的抢票程序
#include<pthread.h>
#include<iostream>
#include<cstdio>
#include<unistd.h>
int counter = 10;
//创建一个全局锁
pthread_mutex_t mutex;
void* start_routine(void* args)
{
while(true){
//在这里加锁,保护公共资源counter
pthread_mutex_lock(&mutex);
if (counter > 0){
counter--;
std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
//这里counter--已经执行完毕,公共资源保护完成,可以解锁
pthread_mutex_unlock(&mutex);
usleep(1234);
}
else{
//这里是程序拿到锁,然而没票了,退出之前要解锁
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main()
{
pthread_t thread;
char second_thread[] = "次线程";
char main_thread[] = "主线程";
//初始化该全局锁
pthread_mutex_init(&mutex, NULL);
pthread_create(&thread, NULL, start_routine, (void*)second_thread);
start_routine((void*)main_thread);
pthread_join(thread, NULL);
return 0;
}
由上面的demo可以看出,加锁和解锁的范围还是很短的,运行上面的demo,你会发现程序抢票的过程明显变慢,这是因为线程在抢票时是串行执行。该demo主要为了保护counter,counter--指令执行完,就可以解锁,避免长时间串行,影响程序效率
这里不给出代码运行结果,尝试自己编写运行
深入理解锁
上面锁的用法也看了,锁确实能保护我们的票目counter,但是你有没有想过,多个线程之间虽然老老实实排队去抢票了,但是它们会竞争锁呀,毕竟谁先拿到锁,谁就能优先执行,多个线程都在竞争同一把锁,那锁是什么?没错,锁也是一种公共资源,那么加锁的过程中是不是也有中间状态,那么谁来保护锁呢?
眼前先别黑,不需要谁来保护锁,操作系统给我们保证了加锁过程是原子性的,也就是加锁过程中不存在中间状态,用一条汇编指令就能完成
实现的方法很多,这里笔者讲一个比较通用的方法
下图是加锁的部分伪代码,也是加锁的核心
我们需要有一个场景,还是用刚才抢票的那个demo,有线程A和B同时抢票
这是锁能实现串行执行的原理,不过上述是线程A一路顺畅的拿到锁,有没有可能
线程A刚执行完movb $0, %al就被切走了呢
线程A刚执行到xchgb %al, mutex就被切走了呢
当然有可能,时间中断可能在任意一条汇编执行时就到来,加下来分别模拟一下这样的场景
上图同时包含了前面提到了两种场景,都没有什么问题,伪代码中最关键的还是xchgb指令,给交换提供了一个绝对原子的操作, 执行xchgb指令,该线程禁止被中断或受到其它线程干扰,对于解锁,要求就没那么高,下面是解锁的伪代码
封装锁
直接使用原生接口创建和调用锁,老是感觉不方便,毕竟是学C++的,就得用C++的方式给其原生接口封装一下,更符合现代编程的使用习惯,封装代码如下
#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
class thread_mutex{
public:
thread_mutex(pthread_mutex_t * mut):_mutex_p(mut)
{
*_mutex_p = PTHREAD_MUTEX_INITIALIZER;
}
void lock()
{
pthread_mutex_lock(_mutex_p);
}
void trylock()
{
pthread_mutex_trylock(_mutex_p);
}
void unlock()
{
pthread_mutex_unlock(_mutex_p);
}
private:
pthread_mutex_t* _mutex_p;
};
#include<unistd.h>
#include"C++_mutex.hpp"
int counter = 100;
pthread_mutex_t mutex;
void* start_routine(void* args)
{
thread_mutex _mutex(&mutex);
while(true){
_mutex.lock();
if (counter > 0){
counter--;
std::cout << "抢票用户:"<< (char*)args << " 剩余票数: " << counter << std::endl;
_mutex.unlock();
usleep(1234);
}
else{
_mutex.unlock();
break;
}
}
return nullptr;
}
int main()
{
pthread_t thread;
char second_thread[] = "次线程";
char main_thread[] = "主线程";
pthread_create(&thread, NULL, start_routine, (void*)second_thread);
start_routine((void*)main_thread);
pthread_join(thread, NULL);
return 0;
}
如果觉得解锁用的麻烦,我们可以利用智能指针的思想,写一个析构函数,把解锁接口调用放到析构函数中,等到类的作用域结束自动解锁,这里就不演示了
小技巧:可以用{ }自定义数据类型的的作用域
死锁
锁真是个好东西啊,线程妈妈(进程)再也不怕小线程无序争夺资源,把一切都搞乱了
于是在多线程中,一旦涉及到公共资源争夺,就加一把锁,但是这么一加,就加出了问题
什么问题呢?看标题也明白了——死锁
起因是这样的,现在有公共资源A和B,公共资源A有一把锁,公共资源B也有一把锁,现在有线程1和2,线程1一开始要使用资源A,于是它把锁A给拿走了,线程2一开始要使用资源B,于是它把锁B给拿走了
线程1执行着突然发现需要资源B了,于是它等待线程2把资源B的锁还回来,不然它没法取资源B呀,可是线程2执行着发现需要资源A了,于是它等待线程1把资源A的锁还回来
线程2哪知道线程1还在等它呢?同样线程1也不知道线程2在等它呀,两个人就这样互相等啊等,没有终止,导致程序无法继续执行,这就是死锁问题
可不是只有两个及以上个锁才会造成死锁问题,一个锁也会造成死锁问题,人也会被自己绊倒,更别说一根筋的电脑了
while(true){
拿这个例子来说,把解锁的接口全给注释了,线程A拿到锁执行完一次循环后,会再次循环,再次申请
加锁,锁本来就在它身上,加锁接口伪代码咱们也看过了,可想而知线程A也将被挂起,且锁也丢了
_mutex.lock();
if (counter > 0){
counter--;
std::cout << "抢票用户:"<< (char*)args << " 剩余票数: " << counter << std::endl;
//_mutex.unlock();
usleep(1234);
}
else{
//_mutex.unlock();
break;
}
}
避免死锁的方法
出现死锁有四个必要条件
1.互斥(某线程拿到锁,那么其它线程要保持互斥,不能取该锁保护的资源)
2.请求与保持:(死锁时,各线程互相请求对方的锁,互不相让)
3.不剥夺:(各线程平等,无权强行夺取其它线程的锁)
4.环路等待条件 (出现死锁问题组成的无法打破僵局的环路)
避免死锁就是不要同时满足上面的四个必要条件,解决死锁的方法就是打破上面的条件之一
例如使用线程的优先级,死锁时优先级低的线程必须主动释放锁
相关的算法有(死锁检测算法,银行家算法等)
线程同步
什么是线程同步?线程不同步会有什么问题呢?其实咱们前面遇到过这个问题,笔者还解释了大半天,遗忘的同学前往文章开头处查看,用到的代码笔者已复制如下
当时笔者解释了半天,为什么要使用usleep(),因为线程不调度,导致票被主线程这一个线程给抢完了,或者是主线程的竞争能力太强了,它在解锁时同时也是离锁最近的,所以它一解锁,立马就循环再拿锁,次线程是一次锁都摸不到
#include<pthread.h>
#include<iostream>
#include<cstdio>
#include<unistd.h>
int counter = 10;
//创建一个全局锁
pthread_mutex_t mutex;
void* start_routine(void* args)
{
while(true){
//在这里加锁,保护公共资源counter
pthread_mutex_lock(&mutex);
if (counter > 0){
counter--;
std::cout << "抢票用户:"<< (char*) args << " 剩余票数: " << counter << std::endl;
//这里counter--已经执行完毕,公共资源保护完成,可以解锁
pthread_mutex_unlock(&mutex);
usleep(1234);
}
else{
//这里是程序拿到锁,然而没票了,退出之前要解锁
pthread_mutex_unlock(&mutex);
break;
}
}
return nullptr;
}
int main()
{
pthread_t thread;
char second_thread[] = "次线程";
char main_thread[] = "主线程";
//初始化该全局锁
pthread_mutex_init(&mutex, NULL);
pthread_create(&thread, NULL, start_routine, (void*)second_thread);
start_routine((void*)main_thread);
pthread_join(thread, NULL);
return 0;
}
你说主线程违背了锁的竞争原则了吗?并没有,它是严格按照锁的竞争规则来的,但是你说这合理吗?这并不合理,主办方把票全部卖给了黄牛,即使你手速逆天,抢票时也只是卡顿一下,然后显示票已卖完
我们要避免票全让一个线程给抢完了,怎么办,简单粗暴的方法就是再加入一个排队系统,刚抢完票的线程不准再抢了,立马去排队,就能让其它的线程有机会抢到票,像这样避免一个线程独占资源而导致其它线程挨饿的就是线程同步(到这里大家应该能体会到,没写过类似的代码,看这部分内容就感觉很虚,一段时间后没有任何印象,所以请务必敲代码,亲自体验这个过程)
生产者与消费者模型
大概了解什么是同步之后,我们来了解一个线程中很重要的模型——生产者与消费者模型
故事来源于生活场景,角色有:顾客,超市,工厂
我们每个人都是顾客,又称为消费者,我们需要商品满足自身生活需求,没有什么好说的。工厂是生产商品的,又称为生产者,有需求,就有人生产嘛,同样没有什么好说的
超市这个角色值得我们聊聊,为什么要有超市呢?
如果顾客直接去工厂买东西不可以吗?顾客作为一个单独的个体,其购买的产品量是很低的,如果每个顾客都来,那每次开机就生产那么一点点东西,赚得钱可能连电费都付不起,同样的,顾客想去买东西,还得等你工厂开机生产,等到生产完,可能天都黑了
可见我们不能把顾客与工厂强关联,需要一个中间区域,在这个中间区域里,顾客随拿随走,工厂可以批量生产有存储区域
可见,超市这个东西就是把顾客和工厂解耦的,如果生产者和消费者强耦合,消费者要等待生产时间,工厂要承担零售成本,有了超市解耦,消费者随拿随走,生产者没有零售成本
大家感觉这个超市的作用熟不熟悉? 不就是咱们前面提到过的缓冲区吗?
顾客消费可以看到超市资源,工厂供货也可以看到超市资源,那么超市不就是公共资源吗?注意指在程序里的公共资源,引入生活中的例子是便于大家理解,但不可把生活中的经验全套入到改模型里来理解该模型
下面看看消费者与生产者之间的关系
消费者与消费者之间:互斥关系
现在超市某个货品只有一个,只有一个消费者可以拿,一个拿了另一个就不能拿了,所以构成互斥关系
生产者与生产者之间:互斥关系
超市的容量是有限的,一个工厂供货了,另一个工厂就不能再供货了,故生产者与生产者之间也构成了互斥关系
生产者与消费者之间:互斥,同步
互斥还是好理解的,生产者正在给超市供货呢,还没开始统计数量呢,消费者直接就进来把商品拿走了,这样是不行的,因此消费者访问公共资源时,生产者就不能访问公共资源,反之亦然,生产者和消费者之间构成互斥关系
假设现在生产者给超市供货,而不断有消费者来购买商品,那么超市和消费者时间都浪费了,也没得到商品。同样,假设没有消费者来超市消费,而不断有生产者前来供货,那么超市和生产者时间都浪费了,也没卖出商品
为了解决这个问题,超市设置了一种同步策略,即有货的时候再通知消费者,没货时消费者不要来,同样,缺货的时候通知生产者,不缺货生产者不要来
上述生产者和消费者模型可以归纳为“321”原则
3种关系(上述的三种关系)
2个角色(生产者线程,消费者线程)
1个交易场所(一段特定结构的缓冲区,即上述超市)
现在有main函数和一个fun函数,main函数调用了fun函数,在正常情况下,main函数调用fun函数之后,fun函数开始执行,而main函数则在等待fun函数执行完毕。同样的main函数还没开始调用fun函数时,fun函数在等待main函数的调用
这里main函数和fun函数就是强耦合的关系,把这两个函数带入到生产者消费者模型
由main函数调用fun函数,所以main函数是参数的提供方(生产者),fun函数是参数的使用方(消费者)如果由两个线程来同时执行这个函数,消费者线程和生产者线程同时开始,同时设立一定的执行条件,当消费者需要数据时,而生产者还没有生产好数据,那么就挂起等待,等到生产者生产好了,满足条件了,就可以唤醒消费者,反之同理
条件变量
什么是条件变量呢? 我们前面提到过,在生产者和消费者模型中,生产者和消费者之间要有互斥和同步的关系,互斥解决方法就是加锁嘛,条件变量就是解决同步的一种常用的方法之一,上面的模型中,超市就是公共资源,消费者消费资源,生产者生产资源,当公共资源没有了,不满足消费者的条件,那么此时消费者不能再来了,老实的挂起等待,反之同理
条件变量维护了一个不满足条件就挂起,满足条件就唤醒的队列,条件变量要和锁一起配合使用,从而满足互斥和同步的关系
所以什么是条件变量呢?其实就是在加锁后,为了维持线程同步,加了一个判断条件,满足条件的可以继续执行,不满足条件的需要挂起等待,条件变量提供了一个队列,这个队列专门用来存放这些不满足条件的线程
消费者线程去超市这个公共资源区去消费,但是此时超市已经没有货物了,所以消费者线程不满足进入超市的条件,去到条件变量里挂起等待,假设此时生产者来了,它检测到超市的货物没有满,可以供货,于是进入了超市这个公共资源区开始供货,同时供货完,它会从条件变量里唤醒一个线程,若该线程是消费者线程,此时判断超市已经有货了,于是它可以进入超市去消费资源了
条件变量的相应接口
条件变量的概念明白了,接下来就看看条件变量如何使用吧。有时你可能会看到笔者在接口和函数之间反复横跳,其实叫接口也好,叫函数也罢,两者是同一个东西,叫函数是从编程语言的角度看待,叫接口是从系统调用的角度来看待的,废话不多说,看接口
条件变量同锁一样,都是一种数据类型,里面包含了各种需要的数据结构
条件变量的数据类型为:pthread_cond_t
声明一个条件变量: pthread_cond_t cond;
同样,声明完要进行初始化
初始化:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);第二个参数填NULL即可
另一种初始化方式:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_destroy(pthread_cond_t *cond);
这个比较好理解,就是把你创建的条件变量销毁
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释该接口的意思是,如果判断条件不满足,那么就调用该接口,系统会把不满足条件的线程放入到条件变量的等待区中
int pthread_cond_signal(pthread_cond_t *cond);
当满足条件之后,通过调用该接口,系统会通知在条件变量里等待的线程,可以开始执行了,注意该接口只能唤醒一个线程int pthread_cond_broadcast(pthread_cond_t *cond);
该接口和上面接口的作用是一样的,不过该接口不只是唤醒一个等待区的线程,而是唤醒一批处在等待变量中的线程
条件变量代码实操
废话不多说,直接用起来,下面是测试条件变量的demo
注:为了简便,后面称呼这些接口调用函数只用后缀代替
例如:pthread_cond_wait() 用wait简称,其它接口类似
int counter = 100;
pthread_mutex_t mutex;
pthread_cond_t cond;
void *start_routine(void *args)
{
thread_mutex _mutex(&mutex);
while (true)
{
_mutex.lock();
if (counter > 0)
{
pthread_cond_wait(&cond, &mutex);
counter--;
std::cout << "抢票用户:" << (char *)args << " 剩余票数: " << counter << std::endl;
_mutex.unlock();
// usleep(1234);
}
else
{
_mutex.unlock();
break;
}
}
return nullptr;
}
int main()
{
pthread_t thread_1, thread_2;
cond = PTHREAD_COND_INITIALIZER;
char name_1[] = "抢票选手1";
char name_2[] = "抢票选手2";
pthread_create(&thread_1, NULL, start_routine, (void *)name_1);
pthread_create(&thread_2, NULL, start_routine, (void *)name_2);
while (true)
{
pthread_cond_signal(&cond);
sleep(1);
}
pthread_join(thread_1, NULL);
pthread_join(thread_2, NULL);
return 0;
}
该代码用到了前面对锁的封装,主要思想就是创建两个子线程,两个子线程刚进入抢票就被wait,进入条件变量挂起等待,然后由主线程用signal逐一唤醒挂起的进程,开始抢票
理解条件变量
等等,老是觉得有些不太对劲,其一在使用条件变量的wait时,为什么要把锁传进去。其二我们不是加锁了嘛,既然加锁了就该保持互斥,也就是只有一个线程才能拿到锁,才能遇到wait,然后进入条件变量等待区,为什么上面的demo思想却说让两个子线程都进入条件变量挂起等待,等待主线程逐一将它们唤醒呢
我们的理解并没有错,只是前面没提到在wait时把锁传过去的作用,wait接口把锁传过去就是为了释放锁,因为释放了锁,其它线程才有机会进入公共资源区。假设现在有一个生产者线程和一个消费者线程,此时公共资源区已经没有资源了,所以消费者进来就得把它挂起等待,如果它把锁也带到等待区而不释放,那么生产者就进入不了公共资源区,那程序就卡死了,生产者永远进不去,消费者永远醒不来
基于BlockingQueue的生产者消费者模型
看标题是不是觉得特别深奥,但仔细看不就是基于阻塞队列的生产者消费者模型嘛,啥意思呢?就是建立一个队列,这个队列的大小是固定的,生产者不断往这个队列中写入数据,消费者不断往这个队列拿出数据,当写入速度明显大于拿出速度时,队列将被写满,就会触发条件判断,把生产者都放到条件变量等待区里。同样的,当拿出速度明显大于写入速度导致队列里没有数据了,会触发条件判断,将消费者放入条件变量的等待区里
了解功能大家可以自行实现,请务必自行实现,下面笔者给出一些参考代码
#include<iostream>
#include<pthread.h>
#include<queue>
#include<vector>
#include<ctime>
#include<cstdlib>
#include<unistd.h>
template< class T>
class thread_cond{
public:
thread_cond()
{
_cond = PTHREAD_COND_INITIALIZER;
}
void thread_cond_push(T data, pthread_mutex_t *mutex)
{
if (_buffer.size() >= _capacity)
{
pthread_cond_wait(&_cond, mutex);
}
_buffer.push(data);
pthread_cond_signal(&_cond);
}
T thread_cond_get(pthread_mutex_t *mutex)
{
if (_buffer.size() <= 0)
{
pthread_cond_wait(&_cond, mutex);
}
T rt_val = _buffer.front();
_buffer.pop();
pthread_cond_signal(&_cond);
return rt_val;
}
~thread_cond()
{
pthread_cond_destroy(&_cond);
}
private:
std::queue<T> _buffer;
pthread_cond_t _cond;
size_t _capacity = 100;
};
----------------------------------------------------------------------------
pthread_mutex_t mutex;
void* start_get(void *args)
{
thread_cond<int> * test_cond = (thread_cond<int>*)args;
while(true)
{
pthread_mutex_lock(&mutex);
int temp = test_cond->thread_cond_get(&mutex);
std::cout << "线程:"<<pthread_self() << "拿出数据" << temp <<std::endl;
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void* start_push(void *args)
{
thread_cond<int> * test_cond = (thread_cond<int>*)args;
while(true)
{
pthread_mutex_lock(&mutex);
int temp = rand()%100;
test_cond->thread_cond_push(temp, &mutex);
std::cout << "线程:"<<pthread_self() << "载入数据" << temp <<std::endl;
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
thread_cond<int> test_cond;
pthread_mutex_init(&mutex, NULL);
std::vector<pthread_t> threads(4);
pthread_create(&threads[0], NULL, start_get, (void*)&test_cond);
pthread_create(&threads[1], NULL, start_get, (void*)&test_cond);
pthread_create(&threads[2], NULL, start_push, (void*)&test_cond);
pthread_create(&threads[3], NULL, start_push, (void*)&test_cond);
for (int i = 0; i < 4; i++)
{
pthread_join(threads[i], NULL);
}
return 0;
}
用模板来编写该类,主要是为了支持泛型,方便日后工作的扩展,现在我们传过去的是int类型的数据,日后可能会是一个自定义类型的类
生产者消费者模型的意义
下图是生产者,消费者模型的简述图
你会不会有这样的疑问,我们引入多线程就是为了实现程序并发,提高工作效率。但是这个生产者消费者模型,消费者想拿数据还是只能加锁一个一个的进入,同样的生产者想放入数据也是只能加锁一个一个的进入,甚至消费者和生产者之间也不能同时进入,那这并发的意义在哪里呢?既然都是一个一个的串入式进入,为何不用单线程呢?
有这样的疑惑是因为我们正在讲述生产者消费者模型,大家都把目光聚焦到这个模型中,但是这个模型仅仅是负责资源的纳入和分配,而不涉及资源的生产和处理
什么意思呢?消费者来到公共资源区是为了拿取资源,而真正消耗时间的是资源的消费过程,但是这个过程并不是在公共资源区内部进行的
同样的,生产者把生产好的数据放入公共资源区,真正消耗时间的是资源的生产过程,这个生产过程也不是在公共资源区进行的
所以多线程并发的意义并不在公共资源区中串型式的拿取增添数据,而是并行式的消费资源和生产资源,加锁是保证资源拿取和增添的安全有效,这个过程并不怎么消耗时间