线程池是一种线程使用模式,用来管理和维护多个线程,避免在短时间内创建和销毁线程的代价,不仅能够保证内核的充分利用,而且能够防止过分调度。
- 需要大量线程完成任务,且完成的时间比较短。
- 对性能要求苛刻的应用。
- 接收突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
接下来我们直接看看源码;
#include "RingQueue.hpp"
#include
#include
#include
#include
#include
using namespace std;
static int maxcap = 10;
static int num = 1;
template
class ThreadPool;
template
class PoolData;
class Thread
{
typedef function func_t;
public:
Thread()
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "Thread->%d", num++);
_name = static_cast(buffer);
}
void start(func_t func, void *args) //_args 是一个指针,内部包含着线程池的指针
{
_func = func;
_args = args;
int n = pthread_create(&_pid, NULL, start_routine, this); // 由于create函数中没有this指针,因此不能直接使用_func
assert(n == 0);
(void)n;
}
static void *start_routine(void *args) // create中的函数不能有this指针,但是它可以用作中转站调用我们想调用的函数
{
Thread *t = static_cast(args);
t->run(t->_args);
}
void *run(void *args)
{
this->_func(args); // 这个_func实际上就是我们线程池中的_handler函数
}
pthread_t _pid;
string _name;
func_t _func;
void *_args;
};
template
class PoolData
{
public:
PoolData(ThreadPool *pool)
: _pool(pool)
{
}
public:
ThreadPool *_pool;
};
template
class ThreadPool
{
public:
ThreadPool(int cap = maxcap)
: _cap(cap)
{
_q = new RingQueue();
pthread_cond_init(&_cond, NULL);
pthread_mutex_init(&_mutex, NULL);
}
// 初始化线程池
void init()
{
for (int i = 0; i < _cap; i++)
{
Thread *t = new Thread();
_v.push_back(t);
}
for (int i = 0; i < _cap; i++)
{
cout << _v[i]->_name << " is read..." << endl;
}
}
static void *_handler(void *args)
{
PoolData *pd = static_cast *>(args);
while (true)
{
T t;
pthread_mutex_lock(&pd->_pool->_mutex);
if (pd->_pool->is_empty())
{
pthread_cond_wait(&pd->_pool->_cond, &pd->_pool->_mutex);
}
pd->_pool->Pop(&t);
cout << "任务已获取 -> " << t << endl;
sleep(1);
pthread_mutex_unlock(&pd->_pool->_mutex);
}
}
bool is_empty()
{
return _q->is_empty();
}
// 线程池运行
void run()
{
for (auto &t : _v)
{
PoolData *pd = new PoolData(this);
t->start(_handler, pd);
cout << t->_name << "start..." << endl;
}
}
// 放入任务
void Push(const T &t)
{
// pthread_mutex_lock(&_mutex);
// _q->push(t);
// pthread_cond_signal(&_cond);
// pthread_mutex_unlock(&_mutex);
_q->Push(t);
}
// 弹出结果
void Pop(T *t)
{
// *t = _q->front();
// _q->pop();
_q->Pop(t);
}
private:
RingQueue* _q;//放任务
// queue *_q;
vector _v; // 放线程
pthread_cond_t _cond;
pthread_mutex_t _mutex;
int _cap; // 最大线程容量
};
为了实现线程和线程池之间的解耦,这里采用了两个类分别保存线程和线程池。
在Thread类中我们保存了线程所需要运行的函数,名字等必要的信息,而线程池内有保存任务和数据的环形队列,保存线程的数组,和保证互斥同步功能的互斥锁和条件变量。
接着来将一些小细节。
在初始化线程池中,我们根据设定的最大变量来new出来我们的新线程,并且放入数组中。
线程池的运行是利用一个循环,并且用一个类对象保存线程池的this指针,然后再调用Thread类中的start函数,将线程池中的_hadler函数和保存线程池this指针的对象传递过去。
为何我们传递过去的函数是线程池中的_handler函数而不是线程原本需要运行的函数呢?
这是因为我们线程类中没有保存数据或任务的队列,只能在线程池中完成任务。
而传递线程池的this指针的原因后续再说明。
start函数的参数是需要运行的函数的指针以及保存了线程池的this指针的指针。
我们把形参赋给类内成员后,便调用我们的create函数。
但是有一点就是,create函数的函数指针不能有this指针,也就是说类内的成员函数必须是static的,否则无法成功调用create函数,因此我们在Thread类内将start_routine函数设为static函数,由于static函数没有this指针, 因此传过去的变量必须是this指针。
在start_routine函数中,我们调用static_cast来进行强转接收this指针,并且用一个Thread类指针对象接收,然后调用类对象的_func函数,并且将t->_args传递过去,这样就能回到线程池中完成回调。
该函数必须是static的,否则在Thread函数中调用_func时会出现报错。
而由于static函数没有this指针,但我们要是想访问临界资源就必须有ThreadPool的this指针,这就是为什么前面的run函数我们一定要传PoolData类对象的指针的原因,然后便是取出任务后完成任务。
不过需要注意的是,我这里的 t 并不是任务,这里我默认使用的是模版类型是 int 类型,若是存储的任务,此处应该按照任务的类或者方式来完成任务。
#include"ThreadPool.hpp"
#include
#include
using namespace std;
int main()
{
srand((unsigned int)time(NULL));
ThreadPool* tp = new ThreadPool();
tp->init();
tp->run();
while(true)
{
int data = rand()%100;
cout<<"data : "<Push(data);
sleep(1);
}
return 0;
}
此处我使用的是int类型,并且每次出现一个随机数就输出并放入线程池中,我们看看结果。
能看到结果正确。
饿汉模式是一开始就创建,不需要申请,我们看看饿汉模式下的线程池吧。
首先最重要的就是在线程池内部创建一个static类型的变量,然后在类外初始化。
其次由于是单例,只能有一个对象,那么就需要将构造函数,拷贝构造,赋值重载之类的函数全部私有化。
最后设一个函数,用来给外面获取该单例对象。
便大功告成了。
#include"ThreadPool.hpp"
#include
#include
using namespace std;
int main()
{
srand((unsigned int)time(NULL));
ThreadPool* tp = ThreadPool::getInstance();
tp->init();
tp->run();
while(true)
{
int data = rand()%100;
cout<<"data : "<Push(data);
sleep(1);
}
return 0;
}
懒汉模式是使用时才申请,我们来看看吧。
既然是申请时再使用,那么就一定是一个指针,用来动态开辟出来的。
而动态开辟就会涉及到线程安全问题,没有锁时外部调用getInstance函数可能会new两个函数。于是就需要一个锁。
并且它的getInstance需要两次判空,防止出现重复new的情况。
其他的就和饿汉模式一样了,看看结果。
发现结果正确。