文章目录
- 一、引入条件变量
- 二、条件变量
- 三、生产者消费者模型
-
- 生产者消费者模型321要素
- 基于BlockingQueue的生产者消费者模型
-
- BlockingQueue
- 单生产者单消费者模型
-
- BlockQueue.hpp代码实现框架
- Enqueue接口设计(理解pthread_cond_wait)
- [Pop接口设计(状态 + 数据分离传递的函数设计思路)](#Pop接口设计(状态 + 数据分离传递的函数设计思路))
- [代码调优(if->while)+ 初始源码](#代码调优(if->while)+ 初始源码)
- 生产消费模型的周边问题
- 传递类对象和函数方法的代码
- 四、条件变量的封装
- 五、阻塞队列的V2版本
一、引入条件变量
短时间进行资源派发如学校选课是可以用互斥锁实现的,但如果我们想让每个线程能公平的获得资源用互斥就会出现问题,因为可能存在竞争锁能力很强的线程,一直抢占着锁资源(一直执行申请锁,释放锁,申请锁...),就会导致两个问题:
1、竞争锁能力很强的线程一直重复执行申请锁,释放锁操作而没有充分利用CPU资源导致是效率低下问题。
2、其他线程由于一直竞争不到锁,进而产生线程饥饿问题。
所以接下来我们要介绍一个新的概念:同步,来补充解决互斥锁存在的问题。在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
二、条件变量
pthread库是通过条件变量来实现的线程同步,我们下面来认识一下条件变量。
条件变量的接口
以pthread开头的函数大部分返回值都是整型,为0表示成功,其他整数表示出错原因。
初始化条件变量
全局初始化条件变量:
cpp
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态 / 局部初始化条件变量(使用完毕后需要调用destroy销毁):
cpp
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
我们会发现条件变量的初始化接口和互斥量几乎一致。
线程在指定条件变量下等待
cpp
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
调用时必须持有锁;
执行时会自动释放锁,让其他线程可以进入临界区; 被唤醒后会自动重新获取锁,然后继续执行。
pthread_cond_wait因为调用时必须持有锁,所以pthread_cond_wait的调用一般都在临界区内,所以pthread_cond_wait一定就会引起线程持锁等待的问题,所以pthread_cond_wait提供了第二个参数,为了让pthread_cond_wait执行时会自动释放锁,大家可能对这里不太理解,后面我们结合具体场景再来看就清楚了。
唤醒在指定条件变量下等待的线程
唤醒一个在指定条件变量下等待的线程(一般唤醒条件变量的等待队列队头线程):
cpp
int pthread_cond_signal(pthread_cond_t *cond);
唤醒所有在指定条件变量下等待的线程:
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
理解条件变量
我们单看上面的条件变量接口可能会感到一头雾水,下面小编讲一个场景来帮助大家理解。
首先有两个眼睛被蒙上了的人,一个盘子,盘子中只能存放一个苹果,一个人负责放苹果到盘子中,一个负责从盘子中拿苹果,它们不论是放苹果还是拿苹果都需要先申请互斥锁,避免一个放一个拿造成数据不一致问题,现在我们站在放苹果的人的视角,当他已经放了一个苹果,后面还想放苹果,他就需要不断的申请锁释放锁来查看苹果是否被拿,如果拿苹果的人迟迟不拿苹果,他就会一直申请锁释放锁一直循环,拿苹果的人想申请锁拿苹果也会受到影响。
所以为了优化这一问题,他们两人约定使用一个铃铛,因为虽然他们眼睛看不到,但是耳朵能听到,当放苹果的人放完苹果后再次申请锁若看到苹果未被拿走,就不再频繁申请锁了,而是到铃铛哪里等待,当拿苹果的人拿完苹果后就会敲响铃铛,这时放苹果的人听到声音就会回到盘子处放苹果,而这里的铃铛就类似于条件变量,人就类似于线程,条件变量本质是一个结构体,内部有一个整型变量用来表示当前条件变量的状态,类似于铃铛是否被敲响。
假设有多个放苹果的人,如果盘子中已经放了苹果,它们就都需要在铃铛处等待拿苹果的人拿苹果,所以条件变量结构体中还有一个等待队列,用于有多个线程同时等待的场景,pthread_cond_signal就是唤醒等待队列的第一个线程,pthread_cond_broadcast就是唤醒等待队列的所有线程。
一个demo代码
active:
1、加锁互斥访问显示器资源(打印)
2、先休眠,由主线程依次唤醒后再打印。
cpp
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
void *active(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond, &gmutex);
std::cout << name << ": active!" << std::endl;
pthread_mutex_unlock(&gmutex);
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, active, (void *)"thread-1");
pthread_create(&t2, nullptr, active, (void *)"thread-2");
pthread_create(&t3, nullptr, active, (void *)"thread-3");
pthread_create(&t4, nullptr, active, (void *)"thread-4");
sleep(5);
while (true)
{
pthread_cond_signal(&gcond);
sleep(1);
}
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
三、生产者消费者模型
要理解上面的demo代码为什么需要加锁和解锁,下面需要介绍一个操作系统学科中的经典模型:生产者消费者模型。
首先我们要理解现实中的生产消费关系,以超市为例,它既不是生产者也不是消费者,因为生产者是各种供应商,消费者是超市顾客,超市本身只是一个缓存商品的场所,那么为什么现实中会有超市模式呢?它有什么作用呢?
1、减少生产和消费过程中产生的成本。
以消费者为例,有了超市消费者要买一根火腿肠只用下楼,成本为火腿肠本身的成本,如果没有超市,消费者要买一根火腿肠需要打车前往火腿肠供应工厂,打车费就是额外成本。
2、支持生产与消费的忙闲不均。
例如过年前供应商非常忙率,消费者比较清闲,过年时供应商会将整个生产线关掉,让员工回家过年,而消费者这是会忙率起来。
3、维护松耦合关系,让生产者和消费者解耦。
顾客要消费时供应商不一定要在同一时间工作,同理供应商生产时不一定需要消费者在同一时间消费。
下面我们来具体分析,前提:本章我们是把超市当作整体来使用,下一章我们学习了信号量后再把超市拆分开使用:拆分成一个一个的货架。
消费者和生产者本质就是线程,它们都要访问超市这个共享资源,就需要对共享资源进行保护,成为临界资源,线程要访问临界资源就需要通过临界区代码。
现在有了生产者和消费者的概念后,那么实践应该如何正确地进行生产和消费呢?本质就是维护生产者和消费者之间的关系。1、生产者之间需要竞争超市这个临界资源,本质就是一种竞争关系,反映在计算机学科视角也就是互斥关系。
2、消费者之间也需要竞争超市这个临界资源,本质就是一种竞争关系,反映在计算机学科视角也是互斥关系。
3、生产者和消费者之间首先也有互斥关系,并且生产者和消费者之间还有一定的顺序性,比如商品满了让消费者来消费,商品空了让生产者来生产,所以生产者和消费者之间还有同步关系。
生产者消费者模型321要素
1、三种关系(生与生,消与消,生与消),两种角色(生和消),一个交易场所(例如超市)。我们要正确实现生产者消费者模型就需要用一个交易场所,让生产者和消费者维护好生产和消费的三种关系。
2、关系和角色我们很好理解,交易场所一般是某种数据结构对象(内存块)。
基于BlockingQueue的生产者消费者模型
下面我们利用条件变量和互斥量模拟实现一个生产者消费者模型,利用这份代码来理解一下前面没有解释清楚的问题:例如条件变量为什么需要互斥锁。
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是⼀种常⽤于实现⽣产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元素;当队列满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
阻塞队列本质有点类似我们在进程间通信时介绍的管道,所以管道本质就是一种进程间的单生产者单消费者模型。

单生产者单消费者模型
自定义类的成员函数参数设计最佳实践:
纯输入参数:const + 引用
纯输出参数:指针
既是输入参数也是输出参数:引用
BlockQueue.hpp代码实现框架
BlockQueue本质是一种容器适配器,它是对STL的queue进行了封装,所以BlockQueue的第一个成员变量是_queue对象。BlockQueue是否为空可以通过queue的empty接口直接判断,但BlockQueue是否满了需要我们手动判断,因为STL容器都是空间满了自动扩容的,所以BlockQueue的第二个成员变量是_cap,表示BlockQueue的容量上限。
对于生产者和消费者线程来说,_queue和_cap是全局的共享资源,所以需要一把互斥锁对共享资源做保护,并且这把互斥锁也可以维护生产与生产、消费与消费、生产与消费之间的互斥关系。所以BlockQueue的第三个成员变量是pthread_mutex_t类型的_lock。
当生产者将队列装满数据后需要到指定条件变量下等待,当消费者将队列数据拿完后也需要到指定条件变量下等待,而对于生产者和消费者来说它们各自需要一个条件变量,这样就可以通过代码指定唤醒生产者或消费者,进而可以完成特定的生产/消费任务。所以BlockQueue的四个和第五个成员变量是_c_cond和_p_cond。
cpp
//BlockQueue.hpp
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
// 被const修饰后该变量仅可读,被static修饰后该全局变量仅可在当前文件中使用
const static uint32_t gcap = 5;
template <typename T>
class BlockQueue
{
public:
BlockQueue(uint32_t cap = gcap)
: _cap(cap)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_c_cond, nullptr);
pthread_cond_init(&_p_cond, nullptr);
}
void Enqueue(const T &in)
{
}
void out(T *in)
{
}
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_c_cond);
pthread_cond_destroy(&_p_cond);
}
private:
// 临界资源
std::queue<T> _bq;
uint32_t _cap; // 容量上限
pthread_mutex_t _lock; // 互斥锁
pthread_cond_t _c_cond; // 专门给消费者提供的条件变量
pthread_cond_t _p_cond; // 专门给生产者提供的条件变量
};
Enqueue接口设计(理解pthread_cond_wait)
1、生产者要进行生产需要调用BlockQueue的Enqueue接口,因为生产过程一定会访问临界资源,所以需要先加锁,生产完成后再解锁。
2、在生产之前需要先判断生产条件是否满足------阻塞队列是否为满,如果满了就需要调用pthread_cond_wait在生产者条件变量下等待,如果没满就可以调用底层queue的push将数据插入阻塞队列中。
cpp
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
if (Isfull()) // 判断生产条件是否满足------队列是否为满
{
pthread_cond_wait(&_p_cond, &_lock);
}
_bq.push(in); //完成生产
pthread_cond_signal(&_c_cond); //唤醒消费者
pthread_mutex_unlock(&_lock);
}
但是这里会出现一个很严重的问题,当生产条件不满足时(阻塞队列已经被放满),生产者会进行等待,但是生产者是在临界区内部等的,也就是生产者是持锁等待的(因为等待时没有释放锁),又因为当前场景中只有一把锁,生产者持锁等待就会导致消费者无法申请锁,无法申请锁就会导致消费者无法从阻塞队列中拿出数据,就会导致阻塞队列一直是满的状态,这样生产者就会无限持锁等待下去,所以pthread_cond_wait的第二个参数需要传一个互斥锁,让线程在等待时自动 释放锁。
我相信大家这时可能会有一个问题,为什么我要把pthread_cond_wait写在临界区内部呢,写在外面不行吗?这里我们需要明白一个点,一般是当线程访问临界资源后发现不满足某种条件后会调用pthread_cond_wait进行等待,比如这里的阻塞队列满了,而访问临界资源这一过程虽然没有对临界资源做修改,但是只要对临界资源做访问,就一定需要事先申请锁,所以在临界区中调用pthread_cond_wait是必然的,是由线程的同步、互斥关系决定的。
当在pthread_cond_wait处等待的线程被唤醒后pthread_cond_wait会自动为等待的线程竞争并持有锁,如果被唤醒的线程没有第一时间竞争到锁会继续等待,直到成功竞争到锁然后才能继续执行临界区代码。
Pop接口设计(状态 + 数据分离传递的函数设计思路)
cpp
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
if (Isempty()) // 判断生产条件是否满足------队列是否为空
{
pthread_cond_wait(&_c_cond, &_lock);
}
*out = _bq.front();
_bq.pop(); //完成消费
pthread_cond_signal(&_p_cond); //唤醒生产者
pthread_mutex_unlock(&_lock);
}
有了前面的说明我相信大家可以很好的理解这里Pop的代码了,这里小编要介绍一种工程中常用的函数设计方式:状态 + 数据分离传递,这里的输出型参数out就是在运用这种设计思路,我们以前返回数据都是直接通过return返回,这样就只传递了数据,如果我们通过传出指针类型的输出型参数,然后通过输出型参数把数据带出来,这样return就能返回函数的操作状态了(比如bool表示成功 / 失败、int表示错误类型),我们以前设计的函数一般都不会出错,而接下来我们会频繁接触多线程代码,而多线程代码的状态不确定性更高,所以以后小编都会通过这种方式设计函数。
(所以上面的函数返回值设为void是不太符合规范的,应该设为bool类型,并且函数结束后需要返回true或者false表示函数是否成功完成任务)
补充:生产者消费者完成生产/消费后都需要唤醒对方,而唤醒对方的函数不论写在临界区内还是临界区外都可以,因为线程一但被唤醒不会立即执行临界区代码,都需要先参与竞争锁,这样就能保证不会出现数据不一致问题。
代码调优(if->while)+ 初始源码
在此之前小编先解释一下之前的一个没有阐述清楚的问题,当只有一把锁的场景中,如果一次性唤醒多个消费者,势必只会有一个消费者竞争到锁,哪些被唤醒的消费者但没有第一时间竞争到锁的消费者会继续等待,那这些被唤醒的消费者会在哪里等待呢?实际上当条件变量和互斥锁配合时,其实存在两个独立的等待队列:
条件变量的等待队列:线程调用pthread_cond_wait后,会先释放锁,然后进入这个队列,等待被signal/broadcast唤醒。
互斥锁的等待队列:线程尝试pthread_mutex_lock但没抢到锁时,会进入这个队列,等待锁被释放。
当抢到锁的消费者执行完操作释放锁后,锁的等待队列里的 9 个消费者,会依次竞争锁并执行后续代码,当此时阻塞队列中数据已经被第一个抢到锁的消费者消费了,如果是if (Isempty()) 条件判断的话9个消费者会继续向后执行消费的代码,但此时消费条件已经不满足了,所以势必会发生错误。所以需要将if判断改为while判断:while(Isempty()),当在锁的等待队列中9个消费者竞争到锁后并不会直接执行消费的代码,而是循环判断消费条件是否满足,若不满足则会继续在条件变量中等待。
下面是源码展示:
cpp
//BlockQueue.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
// 被const修饰后该变量仅可读,被static修饰后该全局变量仅可在当前文件中使用
const static uint32_t gcap = 5;
template <typename T>
class BlockQueue
{
private:
bool Isfull()
{
return _bq.size() >= _cap;
}
bool Isempty()
{
return _bq.empty();
}
public:
BlockQueue(uint32_t cap = gcap)
: _cap(cap)
,_c_wait_num(0)
,_p_wait_num(0)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_c_cond, nullptr);
pthread_cond_init(&_p_cond, nullptr);
}
//生产者进行生产接口
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
while (Isfull()) // 判断生产条件是否满足------队列是否为满
{
_p_wait_num++;
pthread_cond_wait(&_p_cond, &_lock);
_p_wait_num--;
}
_bq.push(in); //完成生产
if(_c_wait_num > 0) //有等待的消费者才执行唤醒
pthread_cond_signal(&_c_cond); //唤醒消费者
pthread_mutex_unlock(&_lock);
}
//消费者进行消费接口
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
while (Isempty()) // 判断生产条件是否满足------队列是否为满
{
_c_wait_num++;
pthread_cond_wait(&_c_cond, &_lock);
_c_wait_num--;
}
*out = _bq.front();
_bq.pop(); //完成消费
if(_p_wait_num > 0) //有等待的生产者才执行唤醒
pthread_cond_signal(&_p_cond); //唤醒生产者
pthread_mutex_unlock(&_lock);
}
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_c_cond);
pthread_cond_destroy(&_p_cond);
}
private:
// 临界资源
std::queue<T> _bq;
uint32_t _cap; // 容量上限
pthread_mutex_t _lock; // 互斥锁
pthread_cond_t _c_cond; // 专门给消费者提供的条件变量
pthread_cond_t _p_cond; // 专门给生产者提供的条件变量
int _c_wait_num; //当前消费者等待个数
int _p_wait_num; //当前生产者等待个数
};
cpp
//main.cc
#include "BlockQueue.hpp"
// 封装一下交易场所
struct ThreadData
{
BlockQueue<int> *bq;
std::string name;
};
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
int data;
td->bq->Pop(&data);
std::cout << "消费者消费了一个数据:" << data << std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
sleep(1);
td->bq->Enqueue(data);
std::cout << "生产者生产了一个数据:" << data++ << std::endl;
}
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>(); // 显式调用默认构造函数
// 定义生产者消费者线程
pthread_t c, p;
ThreadData ctd = {bq, "消费者"};
pthread_create(&c, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"};
pthread_create(&p, nullptr, productor, (void *)&ptd);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
生产消费模型的周边问题
1、上面我们已经实现了单生产者单消费者模型,那如果我们要实现多生产者多消费者模型需要修改代码吗,答案是并不需要,因为多生产者多消费者模型就只用比单生产者单消费者模型多维护生产者和生产者之间的互斥关系和消费者和消费者之间的互斥关系,我们前面提过了一把锁就能解决这个问题。所以上面的代码可以直接在多生产者多消费者场景中使用:
cpp
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>(); // 显式调用默认构造函数
// 定义生产者消费者线程
pthread_t c[2], p[3];
ThreadData ctd = {bq, "消费者"};
pthread_create(c + 0, nullptr, consumer, (void *)&ctd);
pthread_create(c + 1, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"};
pthread_create(p + 0, nullptr, productor, (void *)&ptd);
pthread_create(p + 1, nullptr, productor, (void *)&ptd);
pthread_create(p + 2, nullptr, productor, (void *)&ptd);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
return 0;
}
2、生产和消费不仅仅只停留在传输整型、浮点型等等数据上,也可以传递类对象、传递函数、派发任务等。
3、我们前面谈到生产消费模型是高效的,但是由于互斥和同步机制整个生产消费过程是串行的,高效体现在哪里?这里就不得不点出一点,我们前面介绍和实现的生产消费模型只是整个业务流程的数据交换部分,在交换前生产者需要产生数据,在交换后消费者需要处理数据或者执行任务,高效体现在生产者产生数据和消费者处理数据是并行执行的。(在学习网络后我们就会知道生产者的数据是从网络中来的,生产者会把数据传给消费者然后消费者再对数据做处理)
传递类对象和函数方法的代码
注意"下面没有对BlockQueue.hpp文件中的代码做修改。
传递类对象代码:
cpp
//task.cpp
#include <iostream>
class Task
{
public:
Task()
{
}
Task(int x, int y)
:_a(x)
,_b(y)
{
}
void Excute()
{
_result = _a + _b;
}
void operator()()
{
Excute();
}
void Print()
{
std::cout << _a << "+" << _b << "=" << _result << std::endl;
}
private:
int _a;
int _b;
int _result;
};
cpp
//main.cc
#include "BlockQueue.hpp"
#include "Task.cpp"
// 封装一下交易场所
struct ThreadData
{
BlockQueue<Task> *bq;
std::string name;
};
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
sleep(1);
Task t;
//从交易场所中获取数据或任务
td->bq->Pop(&t);
//处理/消费数据或任务
t();
t.Print();
std::cout << "消费者消费了一个任务" << std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
//生产者的数据/任务来源(以后来源于网络或其他主机)
int x = data;
int y = data + 1;
Task t(x, y);
//数据/任务生产到阻塞队列里
td->bq->Enqueue(t);
std::cout << "生产者生产了一个任务" << std::endl;
data++;
}
}
int main()
{
BlockQueue<Task> *bq = new BlockQueue<Task>();
pthread_t c, p;
ThreadData ctd = {bq, "消费者"};
pthread_create(&c, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"};
pthread_create(&p, nullptr, productor, (void *)&ptd);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
传递具体方法(函数对象)的代码:
cpp
//task.cpp
#include <iostream>
#include <functional>
using func_t = std::function<void()>;
void Printlog()
{
std::cout << "我是一个日志任务" << std::endl;
}
cpp
//main.cc
#include "BlockQueue.hpp"
#include "Task.cpp"
// 封装一下交易场所
struct ThreadData
{
BlockQueue<func_t> *bq;
std::string name;
};
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
sleep(1);
func_t f;
//从交易场所中获取数据或任务
td->bq->Pop(&f);
//处理/消费数据或任务
f();
std::cout << "消费者消费了一个任务" << std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
//生产者的数据/任务来源(以后来源于网络或其他主机)
//数据/任务生产到阻塞队列里
td->bq->Enqueue(Printlog);
std::cout << "生产者生产了一个任务" << std::endl;
data++;
}
}
int main()
{
BlockQueue<func_t> *bq = new BlockQueue<func_t>();
pthread_t c, p;
ThreadData ctd = {bq, "消费者"};
pthread_create(&c, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"};
pthread_create(&p, nullptr, productor, (void *)&ptd);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
四、条件变量的封装
封装条件变量大致思路和封装互斥锁类似,主要关注条件变量的wait接口,因为需要将互斥锁传入,所以我们需要再互斥锁中多加一个get方法,用于获取封装的底层互斥锁。
cpp
//Mutex新增成员函数:
pthread_mutex_t *Get()
{
return &_lock;
}
//Cond.hpp
#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex &lock)
{
pthread_cond_wait(&_cond, lock.Get());
}
void NotifyOne()
{
int n = pthread_cond_signal(&_cond);
(void)n;
}
void NotifyAll()
{
int n = pthread_cond_broadcast(&_cond);
(void)n;
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
五、阻塞队列的V2版本
写这段代码小编踩过的坑:
1、LockGuard lockguard(&_lock); 写成 LockGuard(&_lock);,后者是定义匿名对象,生命周期只有一行。
2、因为在V2版本我们封装了pthread_mutex_t对象和pthread_cond_t对象,所以在BlockQueue的构造函数和析构函数不用手动调用锁和条件变量的init和destroy,因为在定义BlockQueue对象时BlockQueue类的默认构造函数会去调用自定义成员变量Mutex和Cond的默认构造函数,而Mutex和Cond的默认构造函数会自动调用init对被封装的pthread_mutex_t对象和pthread_cond_t对象进行初始化,析构函数同理。但是之前我们写的没对pthread_mutex_t对象和pthread_cond_t对象进行封装的代码在BlockQueue的构造函数和析构函数中必须手动调用init和destroy。
V2版本的优点:极致安全、极致优雅,资源的创建 / 释放完全由对象生命周期托管,永远不会漏写、不会重复操作;即使代码中抛异常,析构函数也会被自动调用,锁和条件变量一定会被销毁;代码可读性更高。
cpp
//V2版本源码
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
#include "Cond.hpp"
// 被const修饰后该变量仅可读,被static修饰后该全局变量仅可在当前文件中使用
const static uint32_t gcap = 5;
template <typename T>
class BlockQueue
{
private:
bool Isfull()
{
return _bq.size() >= _cap;
}
bool Isempty()
{
return _bq.empty();
}
public:
BlockQueue(uint32_t cap = gcap)
: _cap(cap), _c_wait_num(0), _p_wait_num(0)
{
}
void Enqueue(const T &in)
{
{
//临界区
LockGuard lockguard(&_lock);
while (Isfull())
{
_p_wait_num++;
_p_cond.Wait(_lock);
_p_wait_num--;
}
_bq.push(in);
if (_c_wait_num > 0)
_c_cond.NotifyOne();
}
}
void Pop(T *out)
{
LockGuard lockguard(&_lock);
while (Isempty())
{
_c_wait_num++;
_c_cond.Wait(_lock);
_c_wait_num--;
}
*out = _bq.front();
_bq.pop();
if (_p_wait_num > 0)
_p_cond.NotifyOne();
}
~BlockQueue()
{
}
private:
// 临界资源
std::queue<T> _bq;
uint32_t _cap; // 容量上限
Mutex _lock; // 互斥锁
Cond _c_cond; // 专门给消费者提供的条件变量
Cond _p_cond; // 专门给生产者提供的条件变量
int _c_wait_num; // 当前消费者等待个数
int _p_wait_num; // 当前生产者等待个数
};
以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~
