目录
一、模型的基本概念
生产者消费者模型是一种经典的多线程同步问题模型,用于解决线程之间数据共享与同步的问题。它包含三个核心组成部分:
-
生产者(Producer)
-
定义:生产者是负责生成数据的线程或进程。
-
作用:生产者会不断地生成数据,并将这些数据放入一个共享缓冲区中。
-
限制:如果共享缓冲区已经满了,生产者无法再添加数据,必须等待缓冲区有空闲位置。
-
-
消费者(Consumer)
-
定义:消费者是负责从共享缓冲区中取出数据并进行处理的线程或进程。
-
作用:消费者会不断地从共享缓冲区中取出数据,并对这些数据进行处理。
-
限制:如果共享缓冲区为空,消费者无法取出数据,必须等待缓冲区中有数据可供消费。
-
-
共享缓冲区(Buffer)
-
定义:共享缓冲区是一个有限容量的队列,用于存储生产者生成的数据。
-
作用:共享缓冲区是生产者和消费者之间共享的存储空间,用于数据的传递。
-
限制:共享缓冲区的容量是有限的,不能无限扩展。
-
总结一句话:
321原则:3种关系(生产者vs生产者,消费者vs消费者,生产者vs消费者),2种角色(生产者,消费者),1个场所(特定结构的内存空间)
二、模型的核心问题
生产者和消费者在运行过程中会遇到以下核心问题:
-
生产者不能向空缓冲区添加数据
-
问题描述:如果共享缓冲区已经满了,生产者无法再添加数据。
-
解决方案:生产者需要等待,直到缓冲区有空闲位置。这通常通过同步机制(如信号量或条件变量)来实现。
-
-
消费者不能从空缓冲区取数据
-
问题描述:如果共享缓冲区为空,消费者无法取出数据。
-
解决方案:消费者需要等待,直到缓冲区中有数据可供消费。这同样通过同步机制来实现。
-
-
多线程同步问题
-
问题描述:生产者和消费者是并发运行的线程,需要确保对共享缓冲区的访问是线程安全的。
-
解决方案:使用互斥锁(Mutex)或其他同步机制来保护共享缓冲区的访问,避免数据竞争和数据不一致的问题。
-
三、环形队列的实现
(一)数据结构
环形队列是一种先进先出(FIFO)的数据结构,非常适合实现生产者消费者模型。环形队列通过两个指针 _c_pos 和 _p_pos 分别表示消费者和生产者的当前位置,避免了队列头部和尾部的频繁移动,提高了效率。
int _maxsize; // 队列的最大容量
int _c_pos; // 消费者下标
int _p_pos; // 生产者下标
std::vector<T> _ringqueue; // 环形队列
(二)同步机制
为了确保线程安全和高效的线程间通信,我们使用信号量和互斥锁来实现同步机制。
信号量(sem_t)
信号量是一种同步原语,用于控制对共享资源的访问。信号量维护一个计数器,表示可用资源的数量。信号量的操作包括:
-
P 操作(
sem_wait):等待信号量的值大于 0,然后将其减 1。 -
V 操作(
sem_post):将信号量的值加 1。
在生产者消费者模型中,我们使用两个信号量:
-
_cdata_sem:消费者关注的数据资源,初始值为 0。 -
_pspace_sem:生产者关注的空间资源,初始值为队列的最大容量。
sem_t _cdata_sem; // 消费者关注的数据资源
sem_t _pspace_sem; // 生产者关注的空间资源
互斥锁(pthread_mutex_t)
互斥锁用于保护对共享资源的访问,确保同一时间只有一个线程可以修改共享资源。在环形队列中,我们使用两个互斥锁:
-
_c_mutex:保护消费者操作的互斥锁。 -
_p_mutex:保护生产者操作的互斥锁。
pthread_mutex_t _c_mutex; // 消费者互斥锁
pthread_mutex_t _p_mutex; // 生产者互斥锁
(三)生产者操作
生产者通过 push 方法将数据放入队列。push 方法的实现步骤如下:
-
等待空间资源(
_pspace_sem)。 -
加锁后将数据放入队列,并更新生产者指针。
-
增加数据资源(
_cdata_sem)。
void push(const T &in)
{
p(_pspace_sem); // 等待空间资源
lock(_p_mutex); // 加锁
_ringqueue[_p_pos] = in; // 放入数据
_p_pos++; // 更新生产者指针
_p_pos %= _maxsize; // 环形处理
unlock(_p_mutex); // 解锁
v(_cdata_sem); // 增加数据资源
}
关键点解释
-
等待空间资源:
-
生产者在队列满时需要等待,直到有空间可用。通过调用
p(_pspace_sem),生产者线程会阻塞,直到信号量_pspace_sem的值大于 0。
-
-
加锁:
-
使用
lock(_p_mutex)锁定生产者操作,确保线程安全。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。
-
-
放入数据:
-
将数据放入队列的当前位置
_p_pos,然后更新生产者指针_p_pos。通过取模运算_p_pos %= _maxsize,实现环形队列。
-
-
增加数据资源:
-
调用
v(_cdata_sem)增加数据资源,通知消费者队列中有新的数据可用。
-
(四)消费者操作
消费者通过 pop 方法从队列中取出数据。pop 方法的实现步骤如下:
-
等待数据资源(
_cdata_sem)。 -
加锁后从队列中取出数据,并更新消费者指针。
-
增加空间资源(
_pspace_sem)。
void pop(T *out)
{
p(_cdata_sem); // 等待数据资源
lock(_c_mutex); // 加锁
*out = _ringqueue[_c_pos]; // 取出数据
_c_pos++; // 更新消费者指针
_c_pos %= _maxsize; // 环形处理
unlock(_c_mutex); // 解锁
v(_pspace_sem); // 增加空间资源
}
关键点解释
-
等待数据资源:
-
消费者在队列空时需要等待,直到有数据可用。通过调用
p(_cdata_sem),消费者线程会阻塞,直到信号量_cdata_sem的值大于 0。
-
-
加锁:
-
使用
lock(_c_mutex)锁定消费者操作,确保线程安全。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。
-
-
取出数据:
-
从队列的当前位置
_c_pos取出数据,然后更新消费者指针_c_pos。通过取模运算_c_pos %= _maxsize,实现环形队列。
-
-
增加空间资源:
-
调用
v(_pspace_sem)增加空间资源,通知生产者队列中有新的空间可用。
-
(五)信号量操作封装
为了使代码更加简洁易读,我们封装了信号量的 P 操作(p)和 V 操作(v),以及互斥锁的加锁(lock)和解锁(unlock)操作。
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); }
(六)析构函数
在析构函数中,销毁信号量和互斥锁,释放资源。
~RingQueue()
{
sem_destroy(&_cdata_sem); // 销毁消费者信号量
sem_destroy(&_pspace_sem); // 销毁生产者信号量
pthread_mutex_destroy(&_c_mutex); // 销毁消费者互斥锁
pthread_mutex_destroy(&_p_mutex); // 销毁生产者互斥锁
}
四、生产者和消费者的实现
(一)任务类 Task
任务类 Task 用于表示生产者生成的任务。每个任务包含两个操作数、一个操作符和计算结果。通过 run 方法执行任务,并通过 GetTask 和 GetResult 方法分别获取任务的描述和执行结果。
(二)生产者线程
生产者线程负责生成任务,并将任务放入环形队列中。生产者线程的实现步骤如下:
-
随机生成两个操作数和一个操作符,创建一个
Task对象。 -
调用
push方法将任务放入队列。 -
打印生成的任务信息。
void* Productor(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
RingQueue<Task>* rq = td->rq;
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);
rq->push(t);
std::cout << "生产任务的id是: " << name << " 生产了一个任务: " << t.GetTask() << std::endl;
}
}
(三)消费者线程
消费者线程负责从环形队列中取出任务,并执行任务。消费者线程的实现步骤如下:
-
调用
pop方法从队列中取出任务。 -
执行任务。
-
打印任务的执行结果。
void* Consumer(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
RingQueue<Task>* rq = td->rq;
std::string name = td->threadname;
while (true)
{
Task t;
rq->pop(&t);
t.run();
std::cout << "消费任务的id是: " << name << " 任务的结果: " << t.GetResult() << std::endl;
sleep(1);
}
}
(四)主函数
主函数负责初始化环形队列,创建生产者和消费者线程,并启动线程。主函数的实现步骤如下:
-
初始化随机数种子。
-
创建环形队列对象。
-
创建消费者线程和生产者线程。
-
等待线程结束(理论上不会结束)。
-
释放资源。
五、信号量的实现
信号量是一种同步原语,用于控制对共享资源的访问。信号量维护一个计数器,表示可用资源的数量。信号量的操作包括:
-
P 操作(
sem_wait):等待信号量的值大于 0,然后将其减 1。 -
V 操作(
sem_post):将信号量的值加 1。
在生产者消费者模型中,我们使用两个信号量:
-
_cdata_sem:消费者关注的数据资源,初始值为 0。 -
_pspace_sem:生产者关注的空间资源,初始值为队列的最大容量。
(一)信号量的初始化
在环形队列的构造函数中,初始化信号量和互斥锁。
RingQueue(int size = defaultnum)
: _ringqueue(size), _maxsize(size), _c_pos(0), _p_pos(0)
{
sem_init(&_cdata_sem, 0, 0); // 初始化消费者信号量
sem_init(&_pspace_sem, 0, _maxsize); // 初始化生产者信号量
pthread_mutex_init(&_c_mutex, nullptr); // 初始化消费者互斥锁
pthread_mutex_init(&_p_mutex, nullptr); // 初始化生产者互斥锁
}
(二)信号量的操作
封装了信号量的 P 操作(p)和 V 操作(v),以及互斥锁的加锁(lock)和解锁(unlock)操作。
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); }
(三)信号量的销毁
在析构函数中,销毁信号量和互斥锁,释放资源。
~RingQueue()
{
sem_destroy(&_cdata_sem); // 销毁消费者信号量
sem_destroy(&_pspace_sem); // 销毁生产者信号量
pthread_mutex_destroy(&_c_mutex); // 销毁消费者互斥锁
pthread_mutex_destroy(&_p_mutex); // 销毁生产者互斥锁
}
六、环形队列的实现细节
环形队列完整代码:
#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<pthread.h>
const static int defaultnum=5; // 默认队列大小
template<class T>
class RingQueue
{
private:
int _maxsize; // 队列最大容量
int _c_pos; // 消费者下标
int _p_pos; // 生产者下标
std::vector<T>_ringqueue; // 环形队列存储数据
sem_t _cdata_sem; // 消费者关注的数据资源信号量
sem_t _pspace_sem; // 生产者关注的空间资源信号量
pthread_mutex_t _c_mutex; // 消费者互斥锁,用于保护消费者操作队列的临界区
pthread_mutex_t _p_mutex; // 生产者互斥锁,用于保护生产者操作队列的临界区
public:
RingQueue(int size=defaultnum) // 构造函数,初始化队列大小和信号量
:_ringqueue(_maxsize),_maxsize(size),_c_pos(0),_p_pos(0)
{
sem_init(&_cdata_sem,0,0); // 初始化消费者信号量,初始值为0
sem_init(&_pspace_sem,0,_maxsize); // 初始化生产者信号量,初始值为队列大小
pthread_mutex_init(&_c_mutex,nullptr); // 初始化消费者互斥锁
pthread_mutex_init(&_p_mutex,nullptr); // 初始化生产者互斥锁
}
void push(const T& in) // 生产者向队列中添加数据
{
p(_pspace_sem);
lock(_p_mutex);
_ringqueue[_p_pos]=in;
_p_pos++;
_p_pos%=_maxsize;
unlock(_p_mutex);
v(_cdata_sem);
}
void pop(T*out) // 消费者从队列中取出数据
{
p(_cdata_sem);
lock(_c_mutex);
*out=_ringqueue[_c_pos];
_c_pos++;
_c_pos%=_maxsize;
unlock(_c_mutex);
v(_pspace_sem);
}
~RingQueue() // 析构函数,销毁信号量和互斥锁
{
sem_destroy(&_cdata_sem);
sem_destroy(&_pspace_sem);
pthread_mutex_destroy(&_c_mutex);
pthread_mutex_destroy(&_p_mutex);
}
private:
void p(sem_t &sem) // P操作(信号量减1)
{
sem_wait(&sem);
}
void v(sem_t &sem) // V操作(信号量加1)
{
sem_post(&sem);
}
void lock(pthread_mutex_t &mutex) // 加锁
{
pthread_mutex_lock(&mutex);
}
void unlock(pthread_mutex_t &mutex) // 解锁
{
pthread_mutex_unlock(&mutex);
}
};
(一)生产者操作
生产者通过 push 方法将数据放入队列。push 方法的实现步骤如下:
-
等待空间资源(
_pspace_sem)。 -
加锁后将数据放入队列,并更新生产者指针。
-
增加数据资源(
_cdata_sem)。
void push(const T &in)
{
p(_pspace_sem); // 等待空间资源
lock(_p_mutex); // 加锁
_ringqueue[_p_pos] = in; // 放入数据
_p_pos++; // 更新生产者指针
_p_pos %= _maxsize; // 环形处理
unlock(_p_mutex); // 解锁
v(_cdata_sem); // 增加数据资源
}
(二)消费者操作
消费者通过 pop 方法从队列中取出数据。pop 方法的实现步骤如下:
-
等待数据资源(
_cdata_sem)。 -
加锁后从队列中取出数据,并更新消费者指针。
-
增加空间资源(
_pspace_sem)。
void pop(T *out)
{
p(_cdata_sem); // 等待数据资源
lock(_c_mutex); // 加锁
*out = _ringqueue[_c_pos]; // 取出数据
_c_pos++; // 更新消费者指针
_c_pos %= _maxsize; // 环形处理
unlock(_c_mutex); // 解锁
v(_pspace_sem); // 增加空间资源
}
(三)环形队列的处理
环形队列通过取模运算 _p_pos %= _maxsize 和 _c_pos %= _maxsize 实现。当指针达到队列的末尾时,通过取模运算将其重置为队列的开头,从而实现环形队列。
(四)线程安全
生产者线程在将任务放入队列时,会先等待 _pspace_sem 信号量,确保队列中有可用空间。然后,它会加锁 _p_mutex,将任务放入队列,并更新生产者指针 _p_pos。最后,它会增加 _cdata_sem 信号量,通知消费者队列中有新的数据可用。
消费者线程在从队列中取出任务时,会先等待 _cdata_sem 信号量,确保队列中有可用数据。然后,它会加锁 _c_mutex,从队列中取出任务,并更新消费者指针 _c_pos。最后,它会增加 _pspace_sem 信号量,通知生产者队列中有新的空间可用。
生产者和消费者分别使用自己的互斥锁 _p_mutex 和 _c_mutex,避免了生产者和消费者之间的直接冲突。互斥锁的作用是防止多个线程同时修改队列,避免数据竞争。
(五)信号量的作用
信号量用于同步生产者和消费者的行为:
-
_pspace_sem:表示队列中可用的空间数量。初始值为队列的最大容量_maxsize。 -
_cdata_sem:表示队列中可用的数据数量。初始值为 0。
生产者在队列满时等待 _pspace_sem,消费者在队列空时等待 _cdata_sem。当生产者将数据放入队列时,它会增加 _cdata_sem 的值,通知消费者队列中有新的数据可用。当消费者从队列中取出数据时,它会增加 _pspace_sem 的值,通知生产者队列中有新的空间可用。
七、任务类的实现细节
任务类完整代码:
#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() // 析构函数
{
}
};
任务类 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<iostream>
#include<pthread.h>
#include"RingQueue.hpp"
#include"Task.hpp"
#include<ctime>
#include<unistd.h>
#include<string>
using namespace std;
// 线程数据结构,用于传递给生产者和消费者线程
struct ThreadData
{
RingQueue<Task>* rq; // 指向环形队列的指针
std::string threadname; // 线程的名称
};
// 生产者线程函数
void* Productor(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
RingQueue<Task>* rq = td->rq;
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);
rq->push(t);
cout << "生产任务的id是: " << name << " 生产了一个任务: " << t.GetTask() << endl;
}
return nullptr;
}
// 消费者线程函数
void* Consumer(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
RingQueue<Task>* rq = td->rq;
std::string name = td->threadname;
while(true)
{
Task t;
rq->pop(&t);
t();
cout << "消费任务的id是: " << name << " 任务的结果: " << t.GetResult() << endl;
sleep(1);
}
return nullptr;
}
int main()
{
srand(time(nullptr)); // 初始化随机数种子
RingQueue<Task>* rq = new RingQueue<Task>(); // 创建一个环形队列对象
pthread_t c[5], p[3]; // 定义消费者线程数组和生产者线程数组
// 创建5个消费者线程
for(int i = 0; i < 5; i++)
{
ThreadData* td = new ThreadData();
td->rq = rq;
td->threadname = "Consumer-" + std::to_string(i);
pthread_create(c + i, nullptr, Consumer, td);
}
// 创建3个生产者线程
for(int i = 0; i < 3; i++)
{
ThreadData* td = new ThreadData();
td->rq = rq;
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 rq; // 释放环形队列对象
return 0;
}
九、总结
通过上述代码,我们实现了一个基于信号量的生产者消费者模型。关键点包括:
-
环形队列:使用
std::vector实现,通过两个指针管理生产者和消费者的位置,避免了队列头部和尾部的频繁移动,提高了效率。 -
信号量:用于同步生产者和消费者的行为,确保队列操作的线程安全。
-
互斥锁:保护队列操作,防止数据竞争。
-
任务类:封装了任务的生成和执行逻辑。
这种模型适用于多线程环境中的任务调度和数据处理,通过调整队列大小和线程数量,可以优化性能以满足不同需求。
1334

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



