目录
一、模型的基本概念
生产者消费者模型是一种经典的多线程同步问题模型,用于解决线程之间数据共享与同步的问题。它包含三个核心组成部分:
-
生产者(Producer)
-
定义:生产者是负责生成数据的线程或进程。
-
作用:生产者会不断地生成数据,并将这些数据放入一个共享缓冲区中。
-
限制:如果共享缓冲区已经满了,生产者无法再添加数据,必须等待缓冲区有空闲位置。
-
-
消费者(Consumer)
-
定义:消费者是负责从共享缓冲区中取出数据并进行处理的线程或进程。
-
作用:消费者会不断地从共享缓冲区中取出数据,并对这些数据进行处理。
-
限制:如果共享缓冲区为空,消费者无法取出数据,必须等待缓冲区中有数据可供消费。
-
-
共享缓冲区(Buffer)
-
定义:共享缓冲区是一个有限容量的队列,用于存储生产者生成的数据。
-
作用:共享缓冲区是生产者和消费者之间共享的存储空间,用于数据的传递。
-
限制:共享缓冲区的容量是有限的,不能无限扩展。
-
总结一句话:
321原则:3种关系(生产者vs生产者,消费者vs消费者,生产者vs消费者),2种角色(生产者,消费者),1个场所(特定结构的内存空间)
二、模型的核心问题
生产者和消费者在运行过程中会遇到以下核心问题:
-
生产者不能向空缓冲区添加数据
-
问题描述:如果共享缓冲区已经满了,生产者无法再添加数据。
-
解决方案:生产者需要等待,直到缓冲区有空闲位置。这通常通过同步机制(如信号量或条件变量)来实现。
-
-
消费者不能从空缓冲区取数据
-
问题描述:如果共享缓冲区为空,消费者无法取出数据。
-
解决方案:消费者需要等待,直到缓冲区中有数据可供消费。这同样通过同步机制来实现。
-
-
多线程同步问题
-
问题描述:生产者和消费者是并发运行的线程,需要确保对共享缓冲区的访问是线程安全的。
-
解决方案:使用互斥锁(Mutex)或其他同步机制来保护共享缓冲区的访问,避免数据竞争和数据不一致的问题。
-
三、阻塞队列的实现
(一)数据结构
阻塞队列使用 std::queue 作为底层数据结构,通过互斥锁和条件变量实现线程同步。
std::queue<T> _q; // 内部使用 STL 的 queue 实现
int _maxsize; // 队列的最大容量
pthread_mutex_t _mutex; // 互斥锁,用于保护临界区
pthread_cond_t c_cond; // 消费者条件变量
pthread_cond_t p_cond; // 生产者条件变量
int low_water; // 低水位标记
int high_water; // 高水位标记
(二)构造函数
构造函数初始化队列的最大容量、互斥锁和条件变量,并设置低水位和高水位。
BlockQueue(int maxcap = defaultNum)
: _maxsize(maxcap)
{
pthread_mutex_init(&_mutex, nullptr); // 初始化互斥锁
pthread_cond_init(&c_cond, nullptr); // 初始化消费者条件变量
pthread_cond_init(&p_cond, nullptr); // 初始化生产者条件变量
low_water = _maxsize / 3; // 设置低水位
high_water = _maxsize * 2 / 3; // 设置高水位
}
(三)生产者操作
生产者通过 push 方法将数据放入队列。push 方法的实现步骤如下:
-
加锁,确保线程安全。
-
检查队列是否已满。如果队列已满,生产者线程将阻塞,等待条件变量
p_cond的通知。 -
将数据放入队列。
-
如果队列的大小超过了高水位,通知消费者线程。
-
解锁,释放互斥锁。
void push(const T &in)
{
pthread_mutex_lock(&_mutex); // 加锁
while (_q.size() == _maxsize) // 如果队列已满
{
pthread_cond_wait(&p_cond, &_mutex); // 生产者等待
}
_q.push(in); // 将数据加入队列
if (_q.size() > high_water) // 如果队列高于高水位
pthread_cond_signal(&c_cond); // 通知消费者
pthread_mutex_unlock(&_mutex); // 解锁
}
关键点解释
-
加锁:
-
使用
pthread_mutex_lock锁定队列,确保线程安全。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。
-
-
检查队列是否已满:
-
如果队列已满(
_q.size() == _maxsize),生产者线程调用pthread_cond_wait,进入等待状态。pthread_cond_wait会自动释放锁,并将线程置于等待队列中,直到被其他线程唤醒。
-
-
入队操作:
-
当队列有空间时,生产者线程被唤醒,将数据放入队列。
-
-
通知消费者:
-
如果队列的大小超过了高水位(
_q.size() > high_water),调用pthread_cond_signal通知消费者。pthread_cond_signal会唤醒一个等待在消费者条件变量上的线程。
-
-
解锁:
-
完成操作后,释放锁,允许其他线程访问队列。
-
(四)消费者操作
消费者通过 pop 方法从队列中取出数据。pop 方法的实现步骤如下:
-
加锁,确保线程安全。
-
检查队列是否为空。如果队列为空,消费者线程将阻塞,等待条件变量
c_cond的通知。 -
从队列中取出数据。
-
如果队列的大小低于低水位,通知生产者线程。
-
解锁,释放互斥锁。
-
返回取出的数据。
T pop()
{
pthread_mutex_lock(&_mutex); // 加锁
while (_q.size() == 0) // 如果队列为空
{
pthread_cond_wait(&c_cond, &_mutex); // 消费者等待
}
T out = _q.front(); // 获取队列头部数据
_q.pop(); // 移除队列头部数据
if (_q.size() < low_water) // 如果队列低于低水位
pthread_cond_signal(&p_cond); // 通知生产者
pthread_mutex_unlock(&_mutex); // 解锁
return out; // 返回数据
}
关键点解释
-
加锁:
-
使用
pthread_mutex_lock锁定队列,确保线程安全。
-
-
检查队列是否为空:
-
如果队列为空(
_q.size() == 0),消费者线程调用pthread_cond_wait,进入等待状态。pthread_cond_wait会自动释放锁,并将线程置于等待队列中,直到被其他线程唤醒。
-
-
出队操作:
-
当队列有数据时,消费者线程被唤醒,从队列中取出数据。
-
-
通知生产者:
-
如果队列的大小低于低水位(
_q.size() < low_water),调用pthread_cond_signal通知生产者。pthread_cond_signal会唤醒一个等待在生产者条件变量上的线程。
-
-
解锁:
-
完成操作后,释放锁,允许其他线程访问队列。
-
-
返回数据:
-
返回取出的数据。
-
(五)析构函数
在析构函数中,销毁互斥锁和条件变量,释放资源。
~BlockQueue()
{
pthread_mutex_destroy(&_mutex); // 销毁互斥锁
pthread_cond_destroy(&c_cond); // 销毁消费者条件变量
pthread_cond_destroy(&p_cond); // 销毁生产者条件变量
}
四、信号量和条件变量的使用
(一)条件变量
条件变量是一种同步原语,用于线程间的同步。条件变量维护一个等待队列,线程可以在条件不满足时进入等待状态,直到被其他线程唤醒。
-
pthread_cond_wait:等待条件变量。线程在等待时会自动释放锁,并进入等待队列。当线程被唤醒时,它会重新获取锁。 -
pthread_cond_signal:唤醒一个等待在条件变量上的线程。
在阻塞队列中,我们使用两个条件变量:
-
c_cond:消费者条件变量,用于在队列为空时阻塞消费者线程。 -
p_cond:生产者条件变量,用于在队列满时阻塞生产者线程。
(二)互斥锁
互斥锁用于保护对共享资源的访问,确保同一时间只有一个线程可以修改共享资源。在阻塞队列中,我们使用互斥锁 _mutex 保护队列 _q 的访问。
-
pthread_mutex_lock:加锁,确保线程安全。 -
pthread_mutex_unlock:解锁,释放互斥锁。
(三)低水位和高水位
低水位和高水位用于动态调整队列的状态,优化性能:
-
低水位(
low_water):当队列的大小低于低水位时,通知生产者线程。 -
高水位(
high_water):当队列的大小超过高水位时,通知消费者线程。
low_water = _maxsize / 3; // 设置低水位
high_water = _maxsize * 2 / 3; // 设置高水位
五、生产者和消费者的实现
(一)生产者线程
生产者线程负责生成任务,并将任务放入阻塞队列中。生产者线程的实现步骤如下:
-
随机生成两个操作数和一个操作符,创建一个
Task对象。 -
调用
push方法将任务放入队列。 -
打印生成的任务信息。
void* Productor(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
BlockQueue<Task>* bq = td->bq;
std::string name = td->threadname;
while (true)
{
int data1 = rand() % 10 + 1;
int data2 = rand() % 10;
char op = opers[rand() % 5];
Task t(data1, data2, op);
bq->push(t);
std::cout << "生产任务的id是: " << name << " 生产了一个任务: " << t.GetTask() << std::endl;
}
}
(二)消费者线程
消费者线程负责从阻塞队列中取出任务,并执行任务。消费者线程的实现步骤如下:
-
调用
pop方法从队列中取出任务。 -
执行任务。
-
打印任务的执行结果。
void* Consumer(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
BlockQueue<Task>* bq = td->bq;
std::string name = td->threadname;
while (true)
{
Task t;
bq->pop(&t);
t.run();
std::cout << "消费任务的id是: " << name << " 任务的结果: " << t.GetResult() << std::endl;
sleep(1);
}
}
(三)主函数
主函数负责初始化阻塞队列,创建生产者和消费者线程,并启动线程。主函数的实现步骤如下:
-
初始化随机数种子。
-
创建阻塞队列对象。
-
创建消费者线程和生产者线程。
-
等待线程结束(理论上不会结束)。
-
释放资源。
int main()
{
srand(time(nullptr)); // 初始化随机数种子
BlockQueue<Task>* bq = new BlockQueue<Task>();
pthread_t c[5], p[3];
for (int i = 0; i < 5; i++)
{
ThreadData* td = new ThreadData();
td->bq = bq;
td->threadname = "Consumer-" + std::to_string(i);
pthread_create(c + i, nullptr, Consumer, td);
}
for (int i = 0; i < 3; i++)
{
ThreadData* td = new ThreadData();
td->bq = bq;
td->threadname = "Productor-" + std::to_string(i);
pthread_create(p + i, nullptr, Productor, td);
}
for (int i = 0; i < 5; i++)
{
pthread_join(c[i], nullptr);
}
for (int i = 0; i < 3; i++)
{
pthread_join(p[i], nullptr);
}
delete bq;
return 0;
}
六、阻塞队列实现细节
阻塞队列类完整代码:
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
// 定义默认队列大小
template <class T>
class BlockQueue
{
static const int defaultNum = 20; // 默认队列大小
private:
std::queue<T> _q; // 内部使用 STL 的 queue 实现
int _maxsize; // 队列的最大容量
pthread_mutex_t _mutex; // 互斥锁,用于保护临界区
pthread_cond_t c_cond; // 消费者条件变量
pthread_cond_t p_cond; // 生产者条件变量
int low_water; // 低水位标记
int high_water; // 高水位标记
public:
// 构造函数
BlockQueue(int maxcap = defaultNum)
: _maxsize(maxcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&c_cond, nullptr);
pthread_cond_init(&p_cond, nullptr);
low_water = _maxsize / 3;
high_water = _maxsize * 2 / 3;
}
// 生产者调用的 push 方法
void push(const T &in)
{
pthread_mutex_lock(&_mutex);
while (_q.size() == _maxsize) // 判断也是访问临界资源,因此必须要有锁;因为存在伪唤醒的状况,因此要使用while而不是if
{
pthread_cond_wait(&p_cond, &_mutex); // 生产者等待,直到队列有空间
// 注意:pthread_cond_wait 会自动释放锁,并在被唤醒时重新加锁
}
_q.push(in);
if (_q.size() > high_water) // 如果队列大小超过高水位
pthread_cond_signal(&c_cond);
pthread_mutex_unlock(&_mutex);
}
// 消费者调用的 pop 方法
T pop()
{
pthread_mutex_lock(&_mutex);
while (_q.size() == 0) // 判断也是访问临界资源,因此必须要有锁;因为存在伪唤醒的状况,因此要使用while而不是if
{
pthread_cond_wait(&c_cond, &_mutex); // 消费者等待,直到队列有数据
// 注意:pthread_cond_wait 会自动释放锁,并在被唤醒时重新加锁
}
T out = _q.front();
_q.pop();
if (_q.size() < low_water)
pthread_cond_signal(&p_cond);
pthread_mutex_unlock(&_mutex);
return out;
}
// 析构函数
~BlockQueue()
{
pthread_mutex_destroy(&_mutex); // 销毁互斥锁
pthread_cond_destroy(&c_cond); // 销毁消费者条件变量
pthread_cond_destroy(&p_cond); // 销毁生产者条件变量
}
};
(一) 互斥锁的使用
互斥锁用于保护对共享资源的访问,确保同一时间只有一个线程可以修改共享资源。在阻塞队列中,我们使用互斥锁 _mutex 保护队列 _q 的访问。
pthread_mutex_t _mutex; // 互斥锁,用于保护临界区
加锁和解锁
-
加锁:
-
使用
pthread_mutex_lock锁定队列,确保线程安全。 -
互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。
pthread_mutex_lock(&_mutex); -
-
解锁:
-
完成操作后,释放锁,允许其他线程访问队列。
pthread_mutex_unlock(&_mutex); -
(二)条件变量的使用
条件变量用于线程间的同步,允许线程在条件不满足时进入等待状态,直到被其他线程唤醒。
pthread_cond_t c_cond; // 消费者条件变量
pthread_cond_t p_cond; // 生产者条件变量
等待和通知
-
等待条件变量:
-
使用
pthread_cond_wait等待条件变量。线程在等待时会自动释放锁,并进入等待队列。当线程被唤醒时,它会重新获取锁。
复制
pthread_cond_wait(&p_cond, &_mutex); -
-
通知线程:
-
使用
pthread_cond_signal唤醒一个等待在条件变量上的线程。
复制
pthread_cond_signal(&c_cond); -
(三)低水位和高水位
低水位和高水位用于动态调整队列的状态,优化性能:
-
低水位(
low_water):当队列的大小低于低水位时,通知生产者线程。 -
高水位(
high_water):当队列的大小超过高水位时,通知消费者线程。
low_water = _maxsize / 3; // 设置低水位
high_water = _maxsize * 2 / 3; // 设置高水位
(四)生产者操作
生产者通过 push 方法将数据放入队列。push 方法的实现步骤如下:
-
加锁,确保线程安全。
-
检查队列是否已满。如果队列已满,生产者线程将阻塞,等待条件变量
p_cond的通知。 -
将数据放入队列。
-
如果队列的大小超过了高水位,通知消费者线程。
-
解锁,释放互斥锁。
void push(const T &in)
{
pthread_mutex_lock(&_mutex); // 加锁
while (_q.size() == _maxsize) // 如果队列已满
{
pthread_cond_wait(&p_cond, &_mutex); // 生产者等待
}
_q.push(in); // 将数据加入队列
if (_q.size() > high_water) // 如果队列高于高水位
pthread_cond_signal(&c_cond); // 通知消费者
pthread_mutex_unlock(&_mutex); // 解锁
}
关键点解释
-
加锁:
-
使用
pthread_mutex_lock锁定队列,确保线程安全。
-
-
检查队列是否已满:
-
如果队列已满(
_q.size() == _maxsize),生产者线程调用pthread_cond_wait,进入等待状态。pthread_cond_wait会自动释放锁,并将线程置于等待队列中,直到被其他线程唤醒。
-
-
入队操作:
-
当队列有空间时,生产者线程被唤醒,将数据放入队列。
-
-
通知消费者:
-
如果队列的大小超过了高水位(
_q.size() > high_water),调用pthread_cond_signal通知消费者。pthread_cond_signal会唤醒一个等待在消费者条件变量上的线程。
-
-
解锁:
-
完成操作后,释放锁,允许其他线程访问队列。
-
(五)消费者操作
消费者通过 pop 方法从队列中取出数据。pop 方法的实现步骤如下:
-
加锁,确保线程安全。
-
检查队列是否为空。如果队列为空,消费者线程将阻塞,等待条件变量
c_cond的通知。 -
从队列中取出数据。
-
如果队列的大小低于低水位,通知生产者线程。
-
解锁,释放互斥锁。
-
返回取出的数据。
T pop()
{
pthread_mutex_lock(&_mutex); // 加锁
while (_q.size() == 0) // 如果队列为空
{
pthread_cond_wait(&c_cond, &_mutex); // 消费者等待
}
T out = _q.front(); // 获取队列头部数据
_q.pop(); // 移除队列头部数据
if (_q.size() < low_water) // 如果队列低于低水位
pthread_cond_signal(&p_cond); // 通知生产者
pthread_mutex_unlock(&_mutex); // 解锁
return out; // 返回数据
}
关键点解释
-
加锁:
-
使用
pthread_mutex_lock锁定队列,确保线程安全。
-
-
检查队列是否为空:
-
如果队列为空(
_q.size() == 0),消费者线程调用pthread_cond_wait,进入等待状态。pthread_cond_wait会自动释放锁,并将线程置于等待队列中,直到被其他线程唤醒。
-
-
出队操作:
-
当队列有数据时,消费者线程被唤醒,从队列中取出数据。
-
-
通知生产者:
-
如果队列的大小低于低水位(
_q.size() < low_water),调用pthread_cond_signal通知生产者。pthread_cond_signal会唤醒一个等待在生产者条件变量上的线程。
-
-
解锁:
-
完成操作后,释放锁,允许其他线程访问队列。
-
-
返回数据:
-
返回取出的数据。
-
(六)析构函数
在析构函数中,销毁互斥锁和条件变量,释放资源。
~BlockQueue()
{
pthread_mutex_destroy(&_mutex); // 销毁互斥锁
pthread_cond_destroy(&c_cond); // 销毁消费者条件变量
pthread_cond_destroy(&p_cond); // 销毁生产者条件变量
}
七、任务类实现细节
任务类 Task 用于表示生产者生成的任务。每个任务包含两个操作数、一个操作符和计算结果。
任务类完整代码:
#pragma once
#include <iostream>
#include <string>
std::string opers = "+-*/%"; // 定义全局变量,包含所有可能的操作符
class Task
{
private:
int _data1; // 第一个操作数
int _data2; // 第二个操作数
int _result; // 计算结果
int _exitcode; // 退出码,用于表示任务执行的状态
char _operator; // 操作符
public:
Task() // 默认构造函数
{}
// 构造函数,初始化任务对象
Task(int x, int y, char op)
: _data1(x), _data2(y), _result(0), _operator(op), _exitcode(0)
{
}
// 执行任务的逻辑
void run()
{
switch (_operator) // 根据操作符执行不同的计算逻辑
{
case '+': // 加法
_result = _data1 + _data2;
break;
case '-': // 减法
_result = _data1 - _data2;
break;
case '*': // 乘法
_result = _data1 * _data2;
break;
case '/': // 除法
{
if (_data2 == 0) // 检查除数是否为0
_exitcode = 1; // 设置退出码为1,表示除数为0的错误
else
_result = _data1 / _data2;
}
break;
case '%': // 取模
{
if (_data2 == 0) // 检查除数是否为0
_exitcode = 1; // 设置退出码为1,表示除数为0的错误
else
_result = _data1 % _data2;
}
break;
default: // 如果操作符无效
_exitcode = 3; // 设置退出码为3,表示无效操作符
break;
}
}
// 重载 () 运算符,方便直接调用任务对象
void operator()()
{
run(); // 调用 run 方法执行任务
}
// 获取任务的执行结果,格式化为字符串
std::string GetResult()
{
return std::to_string(_data1) + _operator + std::to_string(_data2) + "=(" + std::to_string(_result) + ") [exit code: " + std::to_string(_exitcode) + "]";
}
// 获取任务的描述,格式化为字符串
std::string GetTask()
{
return std::to_string(_data1) + _operator + std::to_string(_data2) + "=?";
}
~Task() // 析构函数
{
}
};
(一)构造函数
任务类的构造函数初始化任务的两个操作数 _data1 和 _data2,以及操作符 _operator。
Task(int x, int y, char op)
: _data1(x), _data2(y), _result(0), _operator(op), _exitcode(0)
{
}
(二)run方法
run 方法根据操作符执行相应的计算,并将结果存储在 _result 中。如果操作符为除法或取模且第二个操作数为零,设置错误码 _exitcode。
void run()
{
switch (_operator)
{
case '+':
_result = _data1 + _data2;
break;
case '-':
_result = _data1 - _data2;
break;
case '*':
_result = _data1 * _data2;
break;
case '/':
if (_data2 == 0)
_exitcode = 1; // 除数为零,设置错误码
else
_result = _data1 / _data2;
break;
case '%':
if (_data2 == 0)
_exitcode = 1; // 除数为零,设置错误码
else
_result = _data1 % _data2;
break;
default:
_exitcode = 3; // 未知操作符,设置错误码
break;
}
}
(三)GetTask 和 GetResult 方法
GetTask 方法返回任务的字符串表示形式,例如 "3+4=?"。GetResult 方法返回任务的执行结果,例如 "3+4=(7) [exit code: 0]"。
std::string GetTask()
{
return std::to_string(_data1) + _operator + std::to_string(_data2) + "=?";
}
std::string GetResult()
{
return std::to_string(_data1) + _operator + std::to_string(_data2) + "=(" + std::to_string(_result) + ") [exit code: " + std::to_string(_exitcode) + "]";
}
八、主函数的实现细节
主函数完整代码:
#include "BlockQueue.hpp"
#include"Task.hpp"
#include<unistd.h>
#include<ctime>
std::string oper="+-*/%";
void* Consumer(void* args)
{
BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(args);
while(true)
{
// 消费
Task t=bq->pop();
// t.run();
t();
std::cout<<"运算结果是:"<<t.GetResult()<<" pthread id is: "<<pthread_self()<<std::endl;
sleep(1);
}
}
void* Productor(void* args)
{
BlockQueue<Task>*bq=static_cast<BlockQueue<Task>*>(args);
while(true)
{
// 生产
int data1=rand()%10;
int data2=rand()%10;
char op=oper[rand()%5];
Task t(data1,data2,op);
bq->push(t);
std::cout<<"生产了一个数据是:"<<t.GetTask()<<" pthread id is: "<<pthread_self()<<std::endl;
}
}
int main()
{
srand(time(nullptr));
BlockQueue<Task>* bq=new BlockQueue<Task>();
pthread_t c[3],p[5];
for(int i=0;i<3;i++)
{
pthread_create(c+i,nullptr,Consumer,bq);
}
for(int i=0;i<5;i++)
{
pthread_create(p+i,nullptr,Productor,bq);
}
for(int i=0;i<3;i++)
{
pthread_join(c[i],nullptr);
}
for(int i=0;i<5;i++)
{
pthread_join(p[i],nullptr);
}
delete bq;
return 0;
}
主函数负责初始化阻塞队列,创建生产者和消费者线程,并启动线程。主函数的实现步骤如下:
- 初始化随机数种子
- 创建阻塞队列对象
- 创建消费者线程
- 创建生产者线程
- 等待线程结束
- 释放资源
7. 总结
通过上述代码,我们实现了一个基于条件变量的阻塞队列,适用于生产者消费者模型。关键点包括:
-
线程安全:通过互斥锁保护队列操作,确保线程安全。
-
条件变量:用于同步生产者和消费者的行为,确保队列操作的线程安全。
-
低水位和高水位:用于动态调整队列的状态,优化性能。
-
生产者和消费者线程:通过阻塞队列实现线程间的通信和同步。
这种模型适用于多线程环境中的任务调度和数据处理,通过调整队列大小和线程数量,可以优化性能以满足不同需求。

被折叠的 条评论
为什么被折叠?



