目录
前言:
认识条件变量
认识接口
快速使用接口
生产消费模型
前言:
前文我们介绍了线程互斥,线程互斥是为了防止多个线程对临界资源访问的时候出现了对一个变量同时操作的情况,对于线程互斥来说,我们使用到了锁,而加锁的过程是原子性的,所以不用担心时间片轮转的时候发生错误,那么加锁的过程为什么是原子的我们也介绍了,因为加锁用到了cpu指令集中的swap指令,直接将内存中的值和寄存器中的值切换,只有一个汇编语句,所以是原子的。
以上是对于线程互斥部分的一个简单总结,本文,我们来介绍线程同步。
介绍线程同步我们这样介绍,从一个生活的简单例子入手,引出条件变量,然后快速的认识条件变量的接口,编写一段测试代码快速使用一下条件变量,最后的大头是生产消费模型,编写完生产消费模型,线程同步就完成了。
那么,进入主题吧!
认识条件变量
我们来假设这么一个场景:VIP自习室(只有一个人能使用),自习室的钥匙,多位同学。
其中的一位同学,从早上持有了自习室的钥匙,别的同学只能在自习室的门外等待该同学放回钥匙,终于,持有钥匙的同学出来放钥匙了,但是该同学又想继续自习,不想放钥匙,就刚把钥匙放下,就立马拿起来了,此时,其他同学刚有一点希望,就来绝望了。
这种情况是不合理的,因为其他同学无法获得钥匙,此时,我们将多个同学看作线程,钥匙是临界资源,所以就导致了饥饿问题。
所以为了合理性,对于钥匙的申请,就有了如下规定:
1->放下钥匙不能立马拿钥匙 2->第二次申请钥匙必须排队
所以,此时自习室的使用就有了一定的顺序性,我们将这种顺序性叫做线程的同步。
通过上文我们好像也没有引出来条件变量呀?
我们再来一个拿苹果的例子,A往盘子里面放苹果,B C从里面拿苹果,那么多个线程之间是独立的,它们怎么知道盘子里面是否有苹果呢?或者说,A怎么知道B C什么时候拿苹果呢?
此时需要一个铃铛吧?当A往盘子里面放了苹果,就敲一下铃铛,此时B C正在排队,B在第一个,拿了苹果就到下一个了,此时顺序性有了,条件变量是什么呢?条件变量其实就是那个铃铛!!
可是,如果没有铃铛会怎么样呢?我们首先要认识到,苹果是临界资源没错,盘子也是吧?那么没有铃铛,就没有人告诉B是否有苹果没有,B就只能自己一直探测,可是一直探测的过程中就是对临界资源的持续访问,换句话说,A就一直访问不到盘子,也就放不到苹果,这效率不就非常非常低了吗?
认识接口
所以,我们需要引出条件变量,铃铛!现在我们对于铃铛有了基本的认识,那么来认识一下它的接口吧!
因为Ubuntu系统无法查看这一类函数,所以想要查看最好使用centos系统或者其他系统查看。
那么这里和互斥锁是十分十分相似的,条件变量分为局部的和全局的,如果使用的是全局的,我们只需要使用宏即可。
如果是局部的,势必要涉及到的函数是初始化和销毁,就是pthread_cond_init 和pthread_cond_destroy函数了,它们的参数也是十分简单的,第一个是pthread_cond_t类型的条件变量指针,第二个参数是属性,我们直接设置为空即可。
当然了,pthread_cond_wait从名字上就看得出来它是要某个线程等待的,至于参数为什么还有锁的参数我们先不管,后面自然会介绍到。
有了铃铛,我们总得敲响铃铛吧?对于函数pthread_cond_signal是唤醒单个线程的,对于函数pthread_cond_broadcast是唤醒在该条件变量下等待的所有线程的,参数也是非常简单,就是条件变量的指针而已。
以上是条件变量我们要使用到的接口,还是十分的简单的吧?
快速使用接口
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>const int nums = 10;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void *wait(void *args)
{std::string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond,&mutex);usleep(10000);std::cout << "I am " << name << std::endl;pthread_mutex_unlock(&mutex);}
}
int main()
{pthread_t threads[nums];// 创建多线程for (int i = 0; i < nums; i++){char *name = new char[128];snprintf(name, 128, "thread'name - %d", i + 1);pthread_create(&threads[i], nullptr, wait, (void *)name);usleep(10000);}// 唤醒线程while (true){pthread_cond_broadcast(&cond);std::cout << "thread wake up ... " << std::endl;sleep(1);}// 等待其他线程for (int i = 0; i < nums; i++){pthread_join(threads[i], nullptr);}return 0;
}
其实使用的话是没有多难的,和之前的互斥锁一模一样,不过是在这里开始我们慢慢的埋下了伏笔而已,比如pthread_cond_wait的第二个参数是什么意思
从上面的代码来说,我们能够理解线程等待的时候,通过函数pthread_cond_wait,在该条件变量下等待,其他的更多是复习了一下之前的内容而已。
生产消费模型
对于这么一个极其简陋的图来说,就是最简单的一个生产消费模型,其中表现的是我们平常都去超市买东西,那么超市是一个生产者吗?
显然,超市不是一个生产者,真正的生产者是供应商,也就是场景,那么消费者不必多说,问题在于,超市本身扮演的地位是什么呢?
实际上,对于超市来说,它更像一种缓冲区,因为消费者不可能买那么多东西,所以一般的模式是厂家给超市,超市给消费者,超市作为缓冲区可以囤积大量的货物。
那么如果没有了超市,会有什么结果呢?
如果没有了超市,消费者直接去厂家买,一次买几包,可是厂家光是打开机器的电都可以买几百包了,如果没有了超市,消费者每次都要和厂家联系,说我们要买什么什么,这样消费者和厂家的联系变的紧密了,是一种降低效率的紧密关系,如果没有了超市,一次性生产了多个商品,那完了,厂家可以下班了,因为消费者根本就买不了那么多东西。
所以有了超市,可以让厂家和一次性生产许多,也不用担心下岗,因为有许多超市作为缓冲区放着,有了超市,可以让厂家和消费者解耦,因为有了中间媒介,既然有了中间商,那么就可以消费者一边消费生产者一边生产,效率也高,请注意,这里的效率也高和一边消费一边生产不是因为所以关系。
那么,我们转接到线程部分来看待生产消费模型,一个一个的消费者和一个一个的生产者看成是一个一个的线程,那么我们可以将生产消费模型总结一下:
一个交易场所->超市
两个角色->生产者和消费者
三个关系->生产者和生产者,消费者和消费者,生产者和消费者
前两个我们是好理解的,那么三个关系我们应该如何理解呢?
生产者和生产者之间,也就是不同的厂家之间,这是竞争关系吧?都不用想,如果没有其他的厂家,只有一个厂家,那么该厂家不知道有多爽。所以生产者和生产者之间是典型的互斥关系。
消费者和消费者之间,实际上也是互斥关系,为什么呢?平常是因为超市的东西足够,所以互斥关系不明显,如果哪天末日爆发,超市的东西有限,你看互斥关系会不会明显起来吧。
生产者和消费者之间,生产者生产好了给超市,消费者从超市里面拿数据,这就是一种顺序性,所以是一种同步关系,可是,如果超市没有东西了,消费者什么也拿不到,也就是对超市这个临界资源的访问必须暂停了,必须要等生产者生产东西给超市,此时消费者相当于就在条件变量下等待了,生产者生产了之后,登记货物的时候消费者也不能拿,必须等货物登记好了再拿,这不就是一种互斥关系吗?
那么实际上,我们面临的生产消费模型,不止有单生产单消费模型,还有多生产多消费模型。
在下篇文章中,我们就要介绍单生产单消费和多生产多消费的代码编写。
下篇文章涉及的是阻塞队列的生产消费模型和环形队列的生产消费模型。
感谢阅读!