文章目录
- 1. 同步的概念
- 2. 条件变量函数
- 2.1 等待函数
- 2.2 样例
- 3. 生产者消费者模型
- 4. 阻塞队列
- 4.1 模拟阻塞队列的生产消费模型
- 4.2 构造函数和析构函数
- 4.3 生产接口和消费接口
- 4.4 创建线程进行测试
1. 同步的概念
互斥可能会导致一个执行流长时间得不到某种资源。也叫饥饿问题。
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
因为时序问题,而导致程序异常,我们称之为竞态条件。
那么同步可以解决饥饿问题,也可以让线程协同。我们要让线程同步就需要条件变量。
2. 条件变量函数
条件变量的目的是:让原本系统唤醒线程改成由程序员自己唤醒。
条件变量的函数和锁的函数是类似的。如果是全局变量或者静态的可以使用宏来定义。如果是局部变量,可以用pthread_cond_init来初始化。
2.1 等待函数
等待条件:
条件变量要和互斥锁mutex一并使用。
唤醒等待:
第一个函数是在这个条件下唤醒一批线程,第二个是在这个条件下唤醒一个线程。
为什么 pthread_cond_wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
这里条件的意思是:共享资源的状态,例如抢票里面的tickets是否大于0。条件变量的意思是:条件满足或不满足的时候,进行wait或signal的一种方式。
2.2 样例
这里我们就开始定义一个条件变量,然后初始化。
三个线程,都会在条件变量下进行排队等待,当我们主线程按顺序去唤醒某个线程,那个线程就会执行打印。
每当我们输入n的时候就唤醒一个线程。
我们可以看到3个线程就按照一定的顺序来打印了。输入其它的就退出了,然后主线程就开始等待其它线程,但其它线程都在阻塞等待,所以进程就不动了。
我们在break之后,把所有的线程都给唤醒,然后终止线程。
我们可以看到还是不行。
我们可以这样设置:
如果我们输入的是n就全部唤醒线程,如果不是就把quit设置成true,然后break,在主线程中再次把全部线程唤醒。这样每个线程都会执行一次run打印,然后判断退出再打印一下end打印。
这样对不对呢?我们看一下运行结果:
我们看到只退出一个线程,其它线程没有退出。
这个和pthread_cond_wait函数有关,在4.3中进行了解释:当全部线程都被唤醒时,就会自动加锁,那么这几个线程都会去抢锁,但只有一个线程能抢到锁,当这个线程退出的时候也把锁带走了。所以其它线程就竞争不到锁了,就退出不了。
解决办法:
那么我们就可以这样修改:
我们手动加锁和解锁。
这样我们就能全部退出了。
还有一种方法:
我们让每个线程分离,它也可以退出。
那么我们也可以定义一个方法集,让不同线程执行不同函数:
加载到vector中,然后让线程去执行:
3. 生产者消费者模型
举个例子:
平时我们在买东西的时候,一般是去超市里购买,而不是直接去生产商里,如果我们去生产商买,还需要等它做好,而去超市就可以直接拿到。这样就提高了效率。另外,我们在买东西的时候,生产商可以不生产,我们不买东西的时候,生产商可以生产,或者我们一边买东西,生产商一边生产。这样就是一种解耦的关系。
在OS中,消费者就是消费线程,生产者就是生产线程,超市是个临界资源,相当于一个缓冲区,平衡了生产者和消费者的处理能力。
1.那么消费者有多个,消费者之间是什么关系呢?
2.生产者有多个,生产者之间是什么关系呢?
这两个问题比较简单,是竞争关系,也就是互斥关系。
3.那么消费者和生产者之间是什么关系呢?
我们知道,如果我们要买个东西,超市没有,那么我们就需要等待生产者,如果没有人买某个东西,那么生产者就需要等消费者,它们是同步关系。那么在生产者给超市货时,消费者不能直接去生产者手中拿,而是等生产者将货放好到超市,消费者才能拿。这也体现了互斥关系。
所以既有互斥关系和同步关系。
总结:
3种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(同步和互斥)。
2种角色:生产者和消费者,由线程承担。
1个交易所:超市,内存中一种特定的数据结构。
4. 阻塞队列
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于:当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,,直到有元素被从队列中取出。
以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞。
4.1 模拟阻塞队列的生产消费模型
在makefie里面我们是可以定义变量的:
然后我们可以用这些变量来替换,格式为$(变量名)。
下面我们就需要完成3个步骤:
1.定义一个阻塞队列。
2.创建两个线程,productor,consumer(生产者和消费者)。
3.让productor和consumer进行联系。
在BlockQueue.hpp里面定义一个阻塞队列。
从上面的原理,我们知道要模拟一个阻塞队列的生产者消费模型,需要阻塞队列,阻塞队列的容量,互斥锁和条件变量。
4.2 构造函数和析构函数
4.3 生产接口和消费接口
生产线程和消费线程它们的工作分为3步:
1.为了保护阻塞队列,我们需要加锁。
2.生产者:判断阻塞队列是否为满,满的话不生产,生产线程休眠。不满的话就生产,并且唤醒消费者。
消费者:判断阻塞队列是否为空,空的话不消费,消费线程休眠。不空的话就消费,并且唤醒生产者。
3.解锁。
首先,我们完成加锁和解锁的封装,这个是比较简单的。封装的意义是如果我们不用queue,可以方便修改。
这是第二步操作,判断阻塞队列满不满足,不满足就阻塞等待。
为什么这里等待的时候需要传mutex_呢?
在阻塞线程的时候,会自动释放mutex_锁。
如果生产者先加锁然后条件不满足,就会阻塞等待,如果不自动释放mutex_锁,就会带着锁等待,那么消费者就不能加锁去消费了。
在唤醒的时候,它也是在这个等待函数后被唤醒,那么在访问临界区的时候是不是就没有锁呢?
当阻塞结束,返回的时候,pthread_cond_wait,会自动帮你重新获得mutex_,然后才返回。
到这里就是条件满足,消费后,把消费的数据返回。
在生产或者消费之后,我们需要唤醒对方。
这里还存在一些问题:
1.pthread_cond_wait函数调用失败了呢?wait失败,没有阻塞成功,继续往下执行。
2.可能由于某种原因进行的伪唤醒,但是现在条件并没有满足。
这样线程可能在条件满足的情况下继续往下执行。
当线程醒来的时候,我们再次去判断条件满不满足,这样代码的健壮性更强。
4.4 创建线程进行测试
现在我们需要创建两个线程productor,consumer让它们进行联系:
我们在这里定义一个随机数种子,异或pid是增加随机性。
我们让生产者2秒生产一个,消费者一直消费,这样消费者就会按照生产者的节奏来。
这里我们再让生产者一次生产多个,然后消费者每隔2秒消费一个。
既然可以使用int类型的数据,我们也可以使用自己封装的类型,包括任务。
这个任务比较简单,是一个计算加减乘除取模的任务。
生产者给阻塞队列放任务,消费者去完成任务。
生产者消费者模型优点是支持并发的,但是上面只体现了互斥,我们该如何理解呢?
那么我们知道:制作任务和处理任务是需要花费时间的,那么就有可能当生产者在生产时,消费者就在消费了。生产者和消费者在阻塞队列中确实是互斥的,因为有锁,但是生产者在生产的时候,消费者在消费的时候,这个是同步的。。