零、对前面的线程函数进行复习并引入条件变量
void Consumer(blockqueue_t &bq)
{
while (true)
{
// 1. 从blockqueue取下来任务
Task t;
bq.Pop(&t);
// 2. 处理这个任务
// t.Execute();
t(); // 消费者私有
// std::cout << "Consumer Consum data is : " << t.ResultToString() << std::endl;
}
}
void Productor(blockqueue_t &bq)
{
srand(time(nullptr) ^ pthread_self());
int cnt = 10;
while (true)
{
sleep(1);
// // 1. 获取任务
// int a = rand() % 10 + 1;
// usleep(1234);
// int b = rand() % 20 + 1;
// Task t(a, b);
// 2. 把获取的任务,放入blockqueue
Task t = PrintHello;
bq.Enqueue(t);
// std::cout << "Productor product data is : " << t.DebugToString() << std::endl;
}
}
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 定义一个全局变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER; // 定义一个全局锁
void *MasterCore(void *args)
{
std::string name = static_cast<const char *>(args);
std::cout << "master 准备工作" << std::endl;
while (true)
{
// 唤醒其中一个队列首部的线程
// pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
std::cout << "master 唤醒一个线程" << std::endl;
sleep(1);
}
}
void StartMaster(std::vector<pthread_t> *tiders)
{
pthread_t pid;
int n = pthread_create(&pid, nullptr, MasterCore, (void *)"Master Thread");
if (n == 0)
{
std::cout << "create master success" << std::endl;
tiders->emplace_back(pid);
}
}
void *SlaverCore(void *args)
{
std::string name = static_cast<const char *>(args);
while (true)
{
// 1. 加锁
pthread_mutex_lock(&gmutex);
// 2. 一般条件变量是在加锁和解锁之间使用的
pthread_cond_wait(&cond, &gmutex); // gmutex:这个是用来被释放的[前一半]
std::cout << "当前被叫醒的线程是:" << name << std::endl;
// 3. 解锁
pthread_mutex_unlock(&gmutex);
}
}
void StartSlaver(std::vector<pthread_t> *tiders, int num)
{
for (int i = 0; i < num; i++)
{
char *name = new char[64];
snprintf(name, 64, "slaver-%d", i + 1);
pthread_t pid;
int n = pthread_create(&pid, nullptr, SlaverCore, (void *)name);
if (n == 0)
{
std::cout << "create Slaver success" << std::endl;
tiders->emplace_back(pid);
}
}
}
void WaitThread(std::vector<pthread_t> &tiders)
{
for (auto &k : tiders)
{
pthread_join(k, nullptr);
}
}
int main()
{
std::vector<pthread_t> tids;
StartMaster(&tids);
StartSlaver(&tids, 5);
WaitThread(tids);
return 0;
}
一、条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个结点添加到队列中,这种情况就需要用到条件变量。
1.1 同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题不难理解。
1.2 条件变量的函数
// 定义一个全局或者静态的变量
pthread_cond_t cond = PTHREAD COND INITIALIZER;
1.2.1 初始化函数——pthread_cond_init
函数的原型:
#include <pthread.h> pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
函数的功能:
初始化一个条件变量,当参数cattr为空指针时,函数创建的是一个缺省的条件变量,否则条件变量的属性将由cattr中的属性值来决定,一般我们都置为nullptr。
函数的参数:
- cv:想要初始化的一个条件变量
- cattr:条件变量的属性,一般置为nullptr
函数的返回值:
- 函数成功返回0
- 任何其他的返回值都表示错误
1.2.2 销毁函数——pthread_cond_destroy
函数的原型:
#include <pthread.h> int pthread_cond_destory(pthread_cond_t* cond);
函数的功能:
用来销毁cond指向的条件变量
函数的参数:
- cond:是指向pthread_cond_t结构的指针
函数的返回值:
- 函数成功返回0
- 任何其他的返回值都表示错误
1.2.3 等待函数——pthread_cond_wait
函数的原型:
#include <pthread.h> int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
函数的功能:
无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现,0表示格林尼治时间1970年1月1日0时0分0秒。无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。mutex互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP)或者适应锁(PTHREAD_MUTEX_ADAPTIVE_NP),且在调用pthread_cond_wait()前必须由本线程加锁(pthread_mutex_lock()),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()之前,mutex将被重新加锁,以与进入pthread_cond_wait()前的加锁动作对应。
函数的参数:
- cond:是指向pthread_cond_t结构的指针
- mutex:互斥锁
函数的返回值:
- 函数成功返回0
- 任何其他的返回值都表示错误
1.2.4 唤醒函数——pthread_cond_broadcast/signal
函数的原型:
#include <pthread.h> int pthread_cond_signal(pthread_cond_t *cond);
函数的功能:
用于向一个条件变量发送信号,通知等待该条件变量的线程
函数的参数:
- cond:是指向pthread_cond_t结构的指针
函数的返回值:
- 函数成功返回0
- 任何其他的返回值都表示错误
1.3 为什么pthread_cond_wait需要互斥量呢??
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故地突然变得满足,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 进入该函数后,检查条件变量等于0不?等于,就把互斥量变成1,直到cond_wait返回,把条件变量改成1,把互斥量恢复成原型。
1.4 条件变量使用规范
1.4.1 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
1.4.2 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
二、生产者消费者模型
生产消费者模型讨论的是:并发数据的传递的问题。我们来用厂商、用户和超市来了解生产者消费者模型。
对于超市来说:超市是一个共享资源——临界资源,是临时保存数据的“内存空间”——某种数据结构对象,也是厂商和用户的数据交易的场所。厂商和用户都是多线程,会有多线程的同步和互斥问题。
在这个模型中有几个并发关系:
- 生产者 VS 生产者 —— 互斥
- 消费者 VS 消费者 —— 互斥
- 生产者 VS 消费者 —— 互斥和同步
321原则(便于记忆):
- 三种关系
- 两个角色
- 一个交易场所
2.1 为什么要使用生产者消费者模型
生产者消费者模型就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者产生完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
2.2 生产者消费者模型优点
- 解耦
- 支持并发
- 支持忙闲不均
2.3 基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于:当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
我们先来创建一个简单的生产者和消费者进程,但是这个两个进程之间是没有关系的,我们需要先将这个两个进程跑起来。
#include <iostream>
#include <string>
#include "Thread.hpp"
#include <vector>
#include <unistd.h>
using namespace ThreadModule;
int a = 10;
void Consumer(int &date)
{
while (date)
{
std::cout << "Consumer" << date-- << std::endl;
sleep(1);
}
}
// 消费者线程需要进行启动
void StartConsumer(std::vector<Thread<int>> *threads, int threadnum)
{
for (int i = 0; i < threadnum; i++)
{
std::string name = "consumer" + std::to_string(i + 1);
threads->emplace_back(Consumer, a, name);
threads->back().Start();
}
}
void Productor(int &date)
{
while (date)
{
std::cout << "productor" << date-- << std::endl;
sleep(1);
}
}
// 生产者线程需要进行启动
void StartProductor(std::vector<Thread<int>> *threads, int threadnum)
{
for (int i = 0; i < threadnum; i++)
{
std::string name = "productor" + std::to_string(i + 1);
threads->emplace_back(Productor, a, name);
threads->back().Start();
}
}
// 主线程最后进行等待各个分线程
void WaitThread(std::vector<Thread<int>> &threads)
{
for (auto &k : threads)
{
k.Join();
}
}
int main()
{
std::vector<Thread<int>> threads;
StartConsumer(&threads, 1); // 生产者线程
StartProductor(&threads, 1); // 消费者线程
WaitThread(threads);
return 0;
}
2.3.1 单生产者单消费者
根据上面的代码,我们需要将生产者和消费者线程通过阻塞队列联系起来。
2.3.1.1 第一个版本
我们先将阻塞队列进行过封装:
首先,我们需要先写出来阻塞队列的属性:
template<class T> class BlockQueue{ private: int _cap; // 记录阻塞队列容量的上限 std::queue<T> _block_queue; // 阻塞队列 pthread_cond_t _productor_cond; // 专门给生产者提供的条件变量 pthread_cond_t _consumer_cond; // 专门给消费者提供的条件变量 pthread_mutex_t _mutex; // 互斥锁 };
其次,我们需要写出阻塞队列中的函数,因为是一个类,我们首先要有构造函数和析构函数,队列需要进行入队列和出队列,我们需要写出来入队列函数和出队列函数。
阻塞队列的构造函数如下:
BlockQueue(int cap) :_cap(cap) // 初始化列表 { // 对互斥锁,条件变量进行初始化 pthread_mutex_init(&_mutex, nullptr); pthread_cond_init(&_productor_cond, nullptr); pthread_cond_init(&_consumer_cond, nullptr); }
阻塞队列的析构函数如下:
~BlockQueue() { // 对互斥锁,条件变量进行销毁 pthread_cond_destory(&_productor_cond); pthread_cond_destory(&_consumer_cond); pthread_mutex_destor(&_mutex); }
阻塞队列的入队列操作函数如下:
void Enqueue(T &in) { // 进行加锁 pthread_mutex_lock(&_mutex); // 检查队列是否已经满了 if(isfull()) { // 让生产者进行等待 // pthread_cond_wait的作用是:1.让调用线程进行等待 // 2.自动释放曾经持有的_mutex锁 // 3.当条件满足时,线程唤醒, //pthread_cond_wait要求线程必须重新竞争锁,竞争成功,方可返回 // 只要等待,必定会有唤醒,唤醒的时候,要继续从这位置向下执行 // 之前,是安全的 pthread_cond_wait(&_productor_cond); // 之后,是安全的,持有锁 } _block_queue.push(in); // 将消费者唤醒 pthread_cond_signal(&_consumer_cond); // 进行解锁 pthread_mutex_unlock(&_mutex); }
阻塞队列的出队列操作函数如下:
void Pop(T *out) { // 进行加锁 pthread_mutex_lock(&_mutex); // 检查队列是否为空 if(isempty()) { // 将消费者进行等待 pthread_cond_wait(&_consumer_cond, &_mutex); } // *out = _block_queue.fonr(); _block_queue.pop(); // 将生产者唤醒 pthread_cond_signal(&_productor_cond); // 进行解锁 pthread_mutex_unlock(&_mutex); }
我们选择来看主函数,主函数中还是三个线程:一个主线程,一个消费者线程,一个生产者线程。
int main()
{
// 这里换成阻塞队列
BlockQueue<int> *bq = new BlockQueue<int>(5);
std::vector<Thread<BlockQueue<int>>> threads;
StartConsumer(&threads, 1, *bq);
StartProductor(&threads, 1, *bq);
WaitThread(threads);
return 0;
}
主线程的工作比较就是遍历存放各个线程的数组,将他们一一释放即可:
void WaitThread(std::vector<Thread<BlockQueue<int>>> &threads)
{
for (auto &k : threads)
{
k.Join();
}
}
线程的启动函数是一样的,我们可以将这个启动函数单独封装一个函数:
void StartCommon(std::vector<Thread<BlockQueue<int>>> *threads,
int threadnum, BlockQueue<int> &bq, func_t<BlockQueue<int>> func)
{
for (int i = 0; i < threadnum; i++)
{
std::string name = "thread-" + std::to_string(i + 1);
threads->emplace_back(func, bq, name);
threads->back().Start();
}
}
之后,就是将消费者和生产者的启动函数各自封装:
void Consumer(BlockQueue<int> &bq)
{
while (true)
{
int date;
bq.Pop(&date);
std::cout << "Consumer Consum date" << date << std::endl;
sleep(2);
}
}
void Productor(BlockQueue<int> &bq)
{
int cnt = 1;
while (true)
{
bq.Enqueue(cnt);
std::cout << "Productor Product date" << cnt << std::endl;
cnt++;
sleep(1);
}
}
void StartConsumer(std::vector<Thread<BlockQueue<int>>> *threads,
int threadnum, BlockQueue<int> &bq)
{
StartCommon(threads, threadnum, bq, Consumer);
}
void StartProductor(std::vector<Thread<BlockQueue<int>>> *threads,
int threadnum, BlockQueue<int> &bq)
{
StartCommon(threads, threadnum, bq, Productor);
}
2.3.1.2 第二个版本
线程的伪唤醒:
如果我们将线程进行整体唤醒,一起去竞争锁,但是只有一个可以成功的竞争,其他操作也是可以正常使用,但是其他的线程也被唤醒后,虽然也会被阻塞休眠,但是不会在条件变量里阻塞休眠,而是在这个互斥锁上进行等待,只能向下走,如果队列中没有元素,会出现错误。
为了使代码更具有健壮性,我们需要将代码做出如下调整:
我们还可以添加两个变量:用来记录生产者和消费者各自有多少个线程在等待,所以新增的代码如下:
#include <iostream>
#include <pthread.h>
#include <queue>
template <class T>
class BlockQueue
{
private:
// 检查是否已经满
bool isfull()
{
return _block_queue.size() == _cap;
}
// 检查是否已经为空
bool isempty()
{
return _block_queue.empty();
}
public:
BlockQueue(int cap)
: _cap(cap)
{
// 给他们进行初始化
_productor_wait_num = 0;
_consumer_wait_num = 0;
pthread_cond_init(&_productor_cond, nullptr);
pthread_cond_init(&_consumer_cond, nullptr);
pthread_mutex_init(&_mutex, nullptr);
}
// 生产者进行入队列操作
void Enqueue(T &in)
{
// 先进行加锁
pthread_mutex_lock(&_mutex);
// 有一个条件变量
// 检查是否为满
if (isfull())
{
_productor_wait_num++;
// 如果已经满了,我们需要将生产者进行等待、
pthread_cond_wait(&_productor_cond, &_mutex);
_productor_wait_num--;
}
// 进行过生产
_block_queue.push(in);
if (_consumer_wait_num)
pthread_cond_signal(&_consumer_cond);
// 最后进行解锁
pthread_mutex_unlock(&_mutex);
}
// 消费者进行出队列操作
void Pop(T *out)
{
// 先进行加锁
pthread_mutex_lock(&_mutex);
// 中间有一个条件变量
// 如果条件为空
if (isempty())
{
// 如果已经空了
_consumer_wait_num++;
pthread_cond_wait(&_consumer_cond, &_mutex);
_consumer_wait_num--;
}
// 进行消费
*out = _block_queue.front();
_block_queue.pop();
if (_productor_wait_num)
pthread_cond_broadcast(&_productor_cond);
// 最后进行解锁
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
// 进行销毁
pthread_cond_destroy(&_productor_cond);
pthread_cond_destroy(&_consumer_cond);
pthread_mutex_destroy(&_mutex);
}
private:
int _cap;
std::queue<T> _block_queue;
pthread_cond_t _productor_cond;
pthread_cond_t _consumer_cond;
pthread_mutex_t _mutex;
int _productor_wait_num;
int _consumer_wait_num;
};
2.3.2 多生产者多消费者
这份代码,我们只需要将数字进行更改就可以变成多生产者多消费者模型,然后,我们再来看一下这份代码,我们不仅仅只能放置整数,而且我们还可以放置函数和类,现在,我们来放置一些类。
对于Task.hpp来说:
class Task
{
public:
Task(){}
Task(int a, int b)
:_a(a)
,_b(b)
,_result(0)
{}
void Execute()
{
_result = _a + _b;
}
private:
int _a;
int _b;
int _result;
};
我们现在只需要将Main.cc的代码进行更改即可:将原本数组中的整数变成一个存放任务的结构体,我们可以根据具体的结构体来进行任务的使用。
2.3.2.1 第一种是支持传递一个类进入线程中:
// 消费者是出栈
void Consumer(BlockQueue<Task> &bq)
{
while (true)
{
Task t;
bq.Pop(&t);
t.Excute();
std::cout << t.ResultToString() << std::endl;
sleep(1);
}
}
// 生产者是入栈
void Productor(BlockQueue<Task> &bq)
{
srand(time(nullptr)); // 种一颗随机数种子
int cnt = 1;
while (true)
{
int a = rand() % 500;
int b = rand() % 1000;
Task t(a, b);
bq.Enqueue(t);
std::cout << "Productor Product task" << std::endl;
sleep(1);
}
}
void StartCommon(std::vector<Thread<BlockQueue<Task>>> *threads, int threadnum, BlockQueue<Task> &bq, func_t<BlockQueue<Task>> func)
{
for (int i = 0; i < threadnum; i++)
{
// 启动线程
std::string name = "thread-" + std::to_string(i + 1);
threads->emplace_back(func, bq, name);
threads->back().Start();
}
}
2.3.2.2 第二个是支持传递一个任务进入线程中:
// 使用一个函数模板定义为一个对象
using Task = std::function<void()>;
// 在主函数中,我们稍微修改一下生产者和消费者的线程过程即可
// 消费者是出队
void Consumer(blockqueue_t &bq)
{
while (true)
{
//
Task t;
bq.Pop(&t);
t();
std::cout << "consumer consume success" << std::endl;
sleep(1);
}
}
// 生产者是入队
void Productor(blockqueue_t &bq)
{
while (true)
{
Task t = Print;
bq.Enqueue(t);
std::cout << "Productor product success" << std::endl;
sleep(1);
}
}
在生产者消费者模型中,对于每一个线程中,我们都进行加锁操作,说明我们这个任务是串行进行的,为什么比较高效,为什么可以提高并发性?
在生产者中进行生产任务,我们需要进行取获取数据,在获取数据的时候,不会影响消费者进行处理任务;同理在消费者进行处理任务时,生产者也在同时的获取数据。虽然,线程是串行的拿任务,但并不代表在处理任务的时候是串行处理的,在处理任务的时候,我们是可以进行并发的。
2.4 POSIX信号量
POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。但是POSIX信号量可以用于线程间同步。
在之前的文章中,我们学习了System V 信号量的相关知识,我们来回顾一下:信号量是用于保护共享资源(临界资源),信号量本质上是一个计数器(衡量资源数目,只要申请成功,就一定有一个对应的资源提供给你),申请信号量的本质就是对公共资源的一种预定机制,信号量分为二元信号量和多元信号量,二元信号量本质上解决了临界资源的互斥问题,二元信号量是一把锁,二元信号量是具有PV操作的。
为什么阻塞队列是被整体使用的??????
在阻塞队列中,为什么保护阻塞队列的时候选择的是加锁,阻塞队列是整体使用的
为什么判断对应的阻塞队列是在加锁和解锁之间内,如果我们在判断的时候突然进行一些操作,我们是需要保护这种情况,所以在整体中加入了锁。
现在,我们学习一下这个信号量的一些函数:
2.4.1 初始化信号量
函数的原型:
函数的功能:
进行初始化信号量
函数的参数:
- sem:指向一个信号对象
- pshared:参数 pshared > 0 时指定了 sem 处于共享内存区域,所以可以在进程间共享该变量
- value:给定一个初始的整数值
函数的返回值:
- sem_init() 成功时返回 0
- 错误时,返回 -1,并把 errno 设置为合适的值
2.4.2 销毁信号量
函数的原型:
函数的功能:
释放信号量自己占用的一切资源 (被注销的信号量sem要求:没有线程在等待该信号量了)
函数的参数:
- sem:指向信号量结构的一个指针
函数的返回值:
- 满足条件 成功返回0
- 失败返回-1且置errno为EBUSY
2.4.3 等待信号量
函数的原型:
函数的功能:
它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值(大于0)才开始做减法。(如果对一个值为0的信号量调用sem_wait(),这个函数就会等待,直到有其它线程增加了信号量这个值使它不再是0为止,再进行减1操作。)
函数的参数:
- sem:指向信号量结构的一个指针
函数的返回值:
- 操作成功返回0
- 失败则返回-1且置errno
2.4.4 发布信号量
函数的原型:
函数的功能:
它的作用来增加信号量的值。给信号量加1。
函数的参数:
- sem:指向信号量结构的一个指针
函数的返回值:
- 操作成功返回0
- 失败则返回-1且置errno
2.5 基于环形队列的生产消费者模型
环形队列采用数组模拟,用模运算来模拟缓装特性。环形队列还是比较简单的数据结构,我们需要将环形队列是如何初始化,判空,判满等一系列操作熟记于心。
环形队列中有以下特性:
- 当队列为空的时候,生产者和消费者的下标在同一个位置。
- 当队列为满的时候,生产者和消费者的下标在同一个位置。
- 将生产者和消费者进行考虑,当生产者和消费者的下标不在同一个位置,一定不为空和一定不为满。
如果,我们使用基于循环队列的生产者消费者模型,根据循环队列的特性,我们可以将这个模型中所有的情况分为三类:
- 当队列为空时,由于生产者和消费者的下标在同一个位置,由于要访问临界资源,我们需要进行互斥和同步的操作,让生产者先跑。根据概率,我们可以得知这个的概率为:。
- 当队列为满时,由于生产者和消费者的下标在同一个位置,由于要访问临界资源,我们需要进行互斥和同步的操作,让消费者先跑。根据概率,我们可以得知这个的概率为:。
- 当队列不空不满时,由于生产者和消费者的下标不指在同一个位置上,我们可以进行同步操作,区别于之前所做的阻塞队列,更加直观地看到了生产者和消费者可以进行同步操作。根据概率,我们可以得知这个概率为:。
根据生产者和消费者的关系以及对循环队列的考虑,我们需要遵守下面两条要求:
- 生产者不能把消费者进行套圈
- 消费者不能超过生产者
2.5.1 对于环形队列的生产者消费者模型的伪代码
我们先来明确一下这个基于环形队列的生产者消费者模型的大致结构:有三个线程,一个主线程,另外两个分别是:生产者线程和消费者线程。在循环队列类中,我们需要创建出循环队列的构造函数、析构函数、出队和入队操作。
而我们所关心的伪代码就是有关出队和入队操作,对于生产者来说,就是入队操作,我们需要将所要执行的任务放入循环队列中,而对于消费者来说,就是出队操作,我们需要将任务从循环队列中取出,然后进行执行操作。
对于生产者,最关心的是空间;
对于消费者,最关心的是数据。
根据上述的伪代码,我们可以将信号量和循环队列进行结合,因为在这个模型中,只会有少量时间会有临界资源问题。
2.5.2 基于环形队列的生产者消费者模型的代码编写
2.5.2.1 单生产单消费模型
基于阻塞队列的生产者和消费者模型的代码基础上,我们来改写成基于环形队列的生产者消费者模型的代码。这个代码中,我们需要编写循环队列类。在这个类中,我们需要定义入队和出队操作,然后根据生产者和消费者的特性将上述的伪代码填入其中。
#pragma once
#include <queue>
#include <iostream>
#include <semaphore.h>
#include <pthread.h>
template <class T>
class RingQueue
{
private:
// 信号量的P操作
void P(sem_t &sem)
{
sem_wait(&sem);
}
// 信号量的V操作
void V(sem_t &sem)
{
sem_post(&sem);
}
public:
// 对于循环队列的构造函数
RingQueue(int cap)
: _ring_queue(cap)
, _cap(cap)
, _consumer_step(0)
, _productor_step(0)
{
sem_init(&_room_sem, 0, _cap);
sem_init(&_date_sem, 0, 0);
// 对锁进行初始化
pthread_mutex_init(&_consumer_mutex, nullptr);
pthread_mutex_init(&_productor_mutex, nullptr);
}
// 需要入队列操作
void Enqueue(const T &in)
{
// 生产者的生产行为
P(_room_sem);
_ring_queue[_productor_step++] = in;
_productor_step %= _cap;
V(_date_sem);
}
// 需要出队列操作
void Pop(T *out)
{
// 消费行为
P(_date_sem);
*out = _ring_queue[_consumer_step++];
_consumer_step %= _cap;
V(_room_sem);
}
~RingQueue() // 析构函数,销毁信号量
{
sem_destroy(&_room_sem);
sem_destroy(&_date_sem);
}
private:
// 1. 定义循环队列及其最大容量
int _cap; // 表示环形队列的容量的上限
std::vector<T> _ring_queue;
// 2. 生产者和消费者的下标
int _consumer_step;
int _productor_step;
// 3. 定义信号量
sem_t _room_sem; // 空间信号量
sem_t _date_sem; // 数据信号量
};
但是,在环形队列中,我们会发现一个小bug,就是在主函数中启动线程时会出现一个重入的现象,错误的代码如下:
void StartCommon(std::vector<Thread<ringqueue_t>> *threads, int threadnum, ringqueue_t &rq, func_t<ringqueue_t> func)
{
for (int i = 0; i < threadnum; i++)
{
// 启动线程
std::string name = "thread-" + std::to_string(i + 1);
threads->emplace_back(func, rq, name);
threads->back().Start();
}
}
如下图所示:如果,我们写在一起,那么在调用Start()函数时,同时线程启动的这个循环中也在启动另一个线程,由于速度较快,在start函数还未执行完,这个线程就已经是另一个线程了,我们需要将这个线程的数据放在start函数中,所以会导致start函数中变量的丢失。
2.5.2.2 多生产多消费模型
我们再来回顾一下生产消费者模型,在前面我们介绍有“321原则”,3指的是三种关系。但是在基于环形队列的单生产单消费模型中只会出现一种关系:生产者和消费者的同步与互斥问题。在多生产多消费模型中,我们会发现会有另外两种关系出现:生产者和生产者的互斥,消费者和消费者的互斥。所以,我们是需要加锁来控制两者的互斥。
完整代码如下:
#pragma once
#include <queue>
#include <iostream>
#include <semaphore.h>
#include <pthread.h>
template <class T>
class RingQueue
{
private:
void P(sem_t &sem)
{
sem_wait(&sem);
}
void V(sem_t &sem)
{
sem_post(&sem);
}
void Lock(pthread_mutex_t *mutex)
{
pthread_mutex_lock(mutex);
}
void Unlock(pthread_mutex_t *mutex)
{
pthread_mutex_unlock(mutex);
}
public:
RingQueue(int cap)
: _ring_queue(cap), _cap(cap), _consumer_step(0), _productor_step(0)
{
sem_init(&_room_sem, 0, _cap);
sem_init(&_date_sem, 0, 0);
// 对锁进行初始化
pthread_mutex_init(&_consumer_mutex, nullptr);
pthread_mutex_init(&_productor_mutex, nullptr);
}
// 需要入队列操作
void Enqueue(const T &in)
{
// 生产者的生产行为
P(_room_sem);
Lock(&_productor_mutex);
_ring_queue[_productor_step++] = in;
_productor_step %= _cap;
Unlock(&_productor_mutex);
V(_date_sem);
}
// 需要出队列操作
void Pop(T *out)
{
// 消费行为
P(_date_sem);
Lock(&_consumer_mutex);
*out = _ring_queue[_consumer_step++];
_consumer_step %= _cap;
Unlock(&_consumer_mutex);
V(_room_sem);
}
~RingQueue()
{
sem_destroy(&_room_sem);
sem_destroy(&_date_sem);
pthread_mutex_destroy(&_productor_mutex);
pthread_mutex_destroy(&_consumer_mutex);
}
private:
int _cap; // 表示环形队列的容量的上限
std::vector<T> _ring_queue;
// 2. 生产和消费的下标
int _consumer_step;
int _productor_step;
// 3.定义信号量
sem_t _room_sem; // 空间信号量
sem_t _date_sem; // 数据信号量
// 4. 定义两把锁
pthread_mutex_t _consumer_mutex;
pthread_mutex_t _productor_mutex;
};