目录
线程池的介绍
基于线程池的生产者消费者模型的模拟实现
线程池类ThreadPool的模拟实现
ThreadPool类的成员变量
ThreadPool类的构造函数和全局的Routine函数
ThreadPool类的析构函数
ThreadPool类的pushTask函数
ThreadPool.h的整体代码
Task.h文件的整体代码
testMain.cc文件的整体代码
对【基于线程池的生产者消费者模型的模拟实现】的测试
首先要知道的是,线程池和进程池一样也是一种池化技术。说简单点就是不要等待若干任务来了再创建若干线程去处理它们,处理完后又销毁这若干个线程;而是不管有无任务,在所有的逻辑开始前首先创建一批线程,等到一个任务过来,我就随便派一个线程去处理它,如果有其他任务,就再派另一个线程去处理它,这些线程处理完任务后不销毁,而是在线程池中等待下一次任务的派发。这样的做法,即线程池的做法会有很明显的优势:
然后要知道的是:其实从上一段就可以看出线程池本质就是一种生产者消费者模型。比如【主线程】或者【像网络这样的外部来源】就可以作为生产者去生产任务,根据生产者消费者模型的理论,生产者就把生产出的任务放进一个交易场所(即可以是通过条件变量实现的阻塞队列或者是通过POSIX信号量实现的环形队列中),然后线程池里的所有线程都可以作为消费者,每次都从交易场所中获取任务后去处理任务。
(如果忘了生产者消费者模型、忘了通过条件变量实现的阻塞队列或者是通过POSIX信号量实现的环形队列,请分别回顾<<生产消费者模型的介绍以及其的模拟实现>>和<
上文中说过了,线程池就是一个生产者消费者模型,让主线程作为生产者去生产任务,把生产出的任务放进一个交易场所(其可以是通过条件变量实现的阻塞队列、也可以是通过信号量实现的环形队列,在当前模拟实现时咱们就选择前者),然后线程池里的所有线程作为消费者,每次都从交易场所中获取任务后去处理任务。
既然线程池ThreadPool是一个基于阻塞队列的生产者消费者模型,那么该类对象就一定有这些成员:
阻塞队列queue
pthread_mutex_t _lock,即分配给阻塞队列(即临界资源或者说交易场所)的锁。需要该锁的原因是需要维护【生产者和消费者的互斥关系】,即防止生产者和消费者同时访问阻塞队列,以避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务导致接取到一个残缺的任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务导致消费者正在接取的任务被覆盖了】。
pthread_cond_t _cond,即分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个消费者线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
vector
int _num,用于统计_v中有多少个线程。
结合上面的理论,我们可以编写出以下代码。
#pragma once
#include
using namespace std;
#include
#include
#include
#include
#include
template
class ThreadPool//用于处理T类型数据的线程池
{
public
private:
vector _v;//线程池
int _num;//统计_v中有多少个线程
queue _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};
构造函数的编写思路:
除了作为生产者的主线程(即执行main函数的线程),其他线程都是被pthread_create创建出来的新线程,这些新线程都是消费者线程,因为所有消费者线程都需要在阻塞队列中接取任务,而我们把这个接取任务的逻辑放在了全局的Routine函数中,所以所有的消费者线程的线程函数就都应该是Routine函数。Routine函数的编写思路是:
结合上面的理论,ThreadPool类的构造函数的代码如下。
#pragma once
#include
using namespace std;
#include
#include
#include
#include
#include
template
class ThreadPool;//前置声明
//线程池中所有的线程(即消费者线程)的例行程序
template
void* routine(void* args)
{
ThreadPool* tp = (ThreadPool*)args;
cout<<"我是消费者线程,线程ID为:"<_lock));
//如果任务队列中为空,即没有任务时,那就让消费者线程在条件变量下陷入阻塞,等待资源就绪
while(tp->_task_queue.empty() == true)
pthread_cond_wait(&(tp->_cond), &tp->_lock);
task = tp->_task_queue.front();
tp->_task_queue.pop();
//消费者线程接取完任务后再解锁并处理刚接取的任务
pthread_mutex_unlock(&(tp->_lock));
cout<<"消费者线程(线程池中的线程都是消费者线程)<"<解决的任务为: "<
class ThreadPool//用于处理T类型数据的线程池
{
template friend void* routine(void* args);
public:
ThreadPool(int num)//num表示线程池中需要多少线程
:_num(num)
{
//是需要在构造函数中初始化锁的
pthread_mutex_init(&_lock,nullptr);
//是需要在构造函数中初始化条件变量的
pthread_cond_init(&_cond,nullptr);
for(int i=0;i, (void*)this);
}
}
private:
vector _v;//线程池
int _num;//统计_v中有多少个线程
queue _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};
析构函数的编写思路:
既然ThreadPool类中有锁和条件变量的成员,那肯定是需要在其的析构函数中销毁它们的;
需要对创建出来的各个线程调用pthread_join函数进行线程等待,即让OS回收掉和线程相关的资源。要注意的是虽然join函数会释放绝大多数和线程相关的资源,但因为线程ID对象(即pthread_t类的对象)是在构造函数中new出来的在堆上的变量,所以需要手动delete释放。
结合上面的理论,ThreadPool类的析构函数的代码如下。
#pragma once
#include
using namespace std;
#include
#include
#include
#include
#include
template
class ThreadPool//用于处理T类型数据的线程池
{
public
~ThreadPool()
{
for(vector::iterator it = _v.begin(); it != _v.end(); it++)
{
//(*it)是vector中存的元素、注意每个元素只是线程ID的地址,而不是线程ID
pthread_join(*(*it), nullptr);
//join函数会释放绝大多数和线程相关的资源,但因为这里的线程ID对象是在构造函数中new出来的在堆上的变量,所以需要手动delete释放。
delete (*it);
}
//是需要在析构函数中销毁锁的
pthread_mutex_destroy(&_lock);
//是需要在析构函数中销毁条件变量的
pthread_cond_destroy(&_cond);
}
private:
vector _v;//线程池
int _num;//统计_v中有多少个线程
queue _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};
该函数是需要被主线程调用的,让作为生产者的主线程往ThreadPool对象中的阻塞队列成员_task_queue中放置任务,所以pushTask的思路为:
结合上面的理论,ThreadPool类的pushTask函数的代码如下。
#pragma once
#include
using namespace std;
#include
#include
#include
#include
#include
template
class ThreadPool//用于处理T类型数据的线程池
{
public
//该函数只被作为生产者的主线程调用
void pushTask(const T& task)
{
pthread_mutex_lock(&_lock);
_task_queue.push(task);
pthread_mutex_unlock(&_lock);
//生产者(即主线程)往交易场所中放置完任务后,需要唤醒某个消费者线程去接取任务
pthread_cond_signal(&_cond);
}
private:
vector _v;//线程池
int _num;//统计_v中有多少个线程
queue _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};
以下是整个ThreadPool.h的代码。
#pragma once
#include
using namespace std;
#include
#include
#include
#include
#include
template
class ThreadPool;//前置声明
//线程池中所有的线程(即消费者线程)的例行程序
template
void* routine(void* args)
{
ThreadPool* tp = (ThreadPool*)args;
cout<<"我是消费者线程,线程ID为:"<_lock));
//如果任务队列中为空,即没有任务时,那就让消费者线程在条件变量下陷入阻塞,等待资源就绪
while(tp->_task_queue.empty() == true)
pthread_cond_wait(&(tp->_cond), &tp->_lock);
task = tp->_task_queue.front();
tp->_task_queue.pop();
//消费者线程接取完任务后再解锁并处理刚接取的任务
pthread_mutex_unlock(&(tp->_lock));
cout<<"消费者线程(线程池中的线程都是消费者线程)<"<解决的任务为: "<
class ThreadPool//用于处理T类型数据的线程池
{
template friend void* routine(void* args);
public:
ThreadPool(int num)//num表示线程池中需要多少线程
:_num(num)
{
//是需要在构造函数中初始化锁的
pthread_mutex_init(&_lock,nullptr);
//是需要在构造函数中初始化条件变量的
pthread_cond_init(&_cond,nullptr);
for(int i=0;i, (void*)this);
}
}
//该函数只被作为生产者的主线程调用
void pushTask(const T& task)
{
pthread_mutex_lock(&_lock);
_task_queue.push(task);
pthread_mutex_unlock(&_lock);
//生产者(即主线程)往交易场所中放置完任务后,需要唤醒某个消费者线程去接取任务
pthread_cond_signal(&_cond);
}
~ThreadPool()
{
for(vector::iterator it = _v.begin(); it != _v.end(); it++)
{
//(*it)是vector中存的元素、注意每个元素只是线程ID的地址,而不是线程ID
pthread_join(*(*it), nullptr);
//join函数会释放绝大多数和线程相关的资源,但因为这里的线程ID对象是在构造函数中new出来的在堆上的变量,所以需要手动delete释放。
delete (*it);
}
//是需要在析构函数中销毁锁的
pthread_mutex_destroy(&_lock);
//是需要在析构函数中销毁条件变量的
pthread_cond_destroy(&_cond);
}
private:
vector _v;//线程池
int _num;//统计_v中有多少个线程
queue _task_queue;//消费者(即线程池中的所有线程,注意线程池中的线程都是非主线程)都需要通过该任务队列获取任务;生产者(即主线程)需要往该任务队列中放置任务。所以本质上这个队列就是生产者消费者模型中的临界资源,形象点说叫交易场所
pthread_mutex_t _lock;//分配给任务队列(即临界资源或者说交易场所)的锁,需要该锁的原因是需要维护【生产者和消费者的互斥关系】,避免发生【生产者放置任务的动作还只做到一半,消费者就跑进交易场所中接取任务】和【消费者接取任务的动作还只做到一半,生产者就跑进交易场所中放置任务】
pthread_cond_t _cond;//分配给消费者(线程池中的所有线程都是消费者)的条件变量,当生产者(即主线程)不断往任务队列中push时,每push一个任务都应该试图唤醒一个线程,避免任务没有消费者接取从而全在任务队列中堆积。注意即使所有消费者线程都在工作,那我唤醒它也没关系,因为此时它会忽略这个唤醒信息,所以这里无脑唤醒即可。
};
首先创建一个Task.h文件,在里面实现一个Task类,这样一来,以后作为生产者的主线程的工作就是不断地创建Task类的对象。Task.h的整体代码如下。
#pragma once
#include
using namespace std;
#include
typedef function func_t;//C++11的包装器
class Task
{
public:
Task()
{}
~Task()
{}
Task(int x,int y,func_t f):_x(x), _y(y), _f(f)
{}
int operator()()
{
return _f(_x,_y);
}
int _x;
int _y;
func_t _f;
};
然后要说的是,有了上文中实现的整个ThreadPool.h的代码后,模拟实现【基于线程池的生产者消费者模型】也就很简单了,思路为:
结合上面的思路,以下是整个testMain.cc的代码。(注意g++编译C++文件时,其后缀名一定得是.cc)
#include"ThreadPool.h"
#include"Task.h"
int main()
{
srand(time(0));
ThreadPool p(3);
while(1)
{
int x = rand()%100;
int y = rand()%100;
Task t(x, y, [](int x, int y)->int{return x+y;});
cout<<"生产者(即主线程)生产出的任务为"<< x << '+' << y << " = ?" << endl;
//每0.7s生产一个任务,防止生产和消费的速度都太快而造成打印语句刷屏
usleep(7000);
p.pushTask(t);
}
return 0;
}
把上面的testMain.cc文件进行编译运行后,结果如下,可以看到是有不同的消费者线程在处理任务的,符合我们的预期。
注意之所以下面是【生产者线程每生产一个任务,消费者线程就能立刻处理一个任务,双方的步调一致】而不是【生产者连续生产一批任务后,消费者再连续消费一批任务,双方的步调不一致】是因为我们在生产者的线程函数(即main函数)中让生产者每生产出一个任务后就usleep阻塞0.7秒。因为我们没有sleep限制消费者线程消费的速度,所以消费者消费的速度就比生产者生产的速度快,但即使消费者消费的速度比生产者生产的速度快,因为不生产是无法消费的,所以双方的步调只能是一致的,所以打印出的结果是步调一致的。