实现一个通用的线程池
前言
如果你对如何实现一个线程池,线程池是什么东西,该怎么设计或为什么别人的线程池要这么设计疑问,可以看看这篇文章,带你从头到尾分析一个线程池怎么实现的以及实现思想
一、什么是线程池,有哪些应用场景
池的概念很容易理解,一块块属性相同的内存汇集叫连接池,一条条tcp连接的汇,集就叫连接池,同样的一条条线程的汇集就叫线程池,但并不是所有线程都能加进来,你的这一个池要做的事情得相同,才能汇集到一块,不能是我是卖瓜的,我手下一百个人都卖瓜,今天来了个卖米的来说要在我手下卖米,我就让它进来了,因为如果让它进来了我这个线程池就不纯粹了,我目前只有卖瓜的方法,没有卖米的方法,当然你要说在我之上有个商业局给我们分配卖米的卖瓜的工作,商业局来管理我们,那你要考虑的就是商业局的线程池怎么设计了。也是一样的思路。 那么线程池有哪些应用场景呢,常见的就是在我们服务器编程中,以及写日志的时候。这些场景的一大特点就是任务与执行分离,什么是任务与执行分离呢,比如服务器编程中我们有连接要接收建立和有数据要接收发送,就是有任务来了,我们要把连接接进来,还要把数据接进来,那么执行呢,我只管帮你你把连接和通过连接来的数据都准备好了,有了这些数据你要做什么,去解析数据,去封装要返回的信息就是一个执行的过程,类似的,写日志,有数据来了外界告诉我要写日志,这就是一个任务,任务发下来了,具体怎么去写的过程,就是执行想的过程。那么外界怎么告诉我们要写日志呢,这就是我们做线程池组件要考虑的,我们要给外界提供一个添加任务的接口。二、线程池的接口分析
属性接口
如第一节介绍的,一般我们一个线程池都包含这三个成员:
1、任务队列
2、执行队列
3、管理者(管理队列)
实际上一般我们的管理者,也就是我们线程池对象本身。
我们一个个来实现。
对于任务队列,我们接口如下:
struct nTask {
//执行任务的函数
void (*task_func)(struct nTask *task);
void *user_data;
struct nTask *prev;
struct nTask *next;
};
正如前面所述,我们一个任务队列至少包含一个执行任务的函数,你在发任务之前你也得把任务怎么做给说清楚,我执行者按你说的去做。
同时也得包含你任务必须的数据user_data,这个user_data可以包含你的任务信息,任务人信息,举个例子,如一个客户到银行存钱,那么这个客户要存钱就是一个任务,这个任务到柜台处理人那里,就是要执行存钱的操作,而存钱必须存款人信息。
对于执行队列,我们也先来看看代码:
struct nWorker {
pthread_t threadid;
int terminate;
//方便工作时能操作管理对象,这种写法其实很方便实现c++类的成员函数可以直接操作类的成员变量
//因为这里要共享mutex和cond,而在这种情况下通过一个manager指针就可以获得线程管理的锁
//线程池的线程实际上就是对应一个worker,每个work可以执行不同任务,但多个worker之间要有一定同步,大家就约定同步的为manager
//每个人都得知道怎么联系manager,因为manager掌握各个worker和task的总情况。
//worker的工作都一样,就是去检查(manager管理的)任务队列有没有任务,如果有任务,就把任务从manager中取出来执行
//执行的方法也是封装在每一个task中,(外界)在将每一个task提交给管理者,因为我们提供的是一个组件,要给其他人提供接口的,所以往我们线程池添加任务也需要接口
//就像经理接收外界的业务一样,这里任务也是添加到管理者的任务队列,任务要包含任务信息,执行方法,(任务信息可能包含提交者,双方约定好的一些必须数据,如银行可能就是存取额
//以及任务的处理方法,实际上在任务队列的封装中,完全可以根据提交信息,来对应封装执行方法,对worker也是可以对应提交信息来对应执行,这里是告诉worker你就得按照我封装好的方法去调用)
struct nManager *manager;
struct nWorker *prev;
struct nWorker *next;
};
对于一个执行队列,我们也有我们自身的id,这里就是线程id,我一个执行者可以执行多个任务,就像柜员可以存钱,也可以取钱,还可以开户,销户等。以及我什么时候不再执行任务了,我有一个结束标志。那么我执行的时候我怎么去获取任务呢,因为这里我们任务队列是通过manager来进行管理的,所以我们是通过manager来访问到任务队列,因为对于每一个执行队列来说,任务队列是他们公共的资源,就像一个人可以选择去柜员1,也可以去柜员2那里处理。
我们再来看看管理者要包含哪些东西:
typedef struct nManager {
//指向任务队列的首节点
//外界派任务来才初始化
struct nTask *tasks;
//指向执行队列的首节点
struct nWorker *workers;
pthread_mutex_t mutex;
//因为要等待任务的到来,任务到来后条件满足,所以要用到条件变量
pthread_cond_t cond;
} ThreadPool; //线程池就是管理对象
首先是管理者得知道我有哪些任务吧,所以要有一个任务队列的入口,实际上一个管理者也就是一个线程池对象,这里既是任务队列入口,也是线程池的任务队列本身,类似的我也得知道我的有哪些执行队列,我有多少人我得知道吧,所以我有一个执行队列入口,也是执行队列本身,此外,我们不能在一个客户已经在柜员1已经办任务的时候还叫他去柜员3办业务吧,所以我们要有一个同步的方法,就是我们的互斥锁和条件变量
操作接口
我们要记住我们做的是组件,是要能提供给别人用的或其他模块用的,那么我们就得封装函数,来给其他人用。
首先别人要用我们的线程池,得先初始化以下,所以我们要有一个初始化的操作:
int nThreadPoolCreate(ThreadPool *pool, int numWorkers) {
if (pool == NULL) return -1;
if (numWorkers < 1) numWorkers = 1;
memset(pool, 0, sizeof(ThreadPool));
pthread_cond_t blank_cond = PTHREAD_COND_INITIALIZER;
memcpy(&pool->cond, &blank_cond, sizeof(pthread_cond_t));
//pthread_mutex_init(&pool->mutex, NULL);
pthread_mutex_t blank_mutex = PTHREAD_MUTEX_INITIALIZER;
memcpy(&pool->mutex, &blank_mutex, sizeof(pthread_mutex_t));
int i = 0;
for (i = 0;i < numWorkers;i ++) {
struct nWorker *worker = (struct nWorker*)malloc(sizeof(struct nWorker));
if (worker == NULL) {
perror("malloc");
return -2;
}
memset(worker, 0, sizeof(struct nWorker));//堆上分配的数据首先要先置0
worker->manager = pool; //
int ret = pthread_create(&worker->threadid, NULL, nThreadPoolCallback, worker);
if (ret) {
perror("pthread_create");
free(worker);
return -3;
}
LIST_INSERT(worker, pool->workers);
}
// success
return 0;
}
我们初始化的东西,就是初始化我们管理成员中的锁,条件变量,及我们的执行队列(工作队列),我们创建预定数目的工作线程来工作,每创建一条工作线程,就往管理成员中的执行队列中插入当前刚创建的的执行者信息。
有初始化对应的也由释放,我们也需要有对应的销毁操作:
代码如下(示例):
int nThreadPoolDestory(ThreadPool *pool, int nWorker) {
struct nWorker *worker = NULL;
for (worker = pool->workers;worker != NULL;worker = worker->next) {
worker->terminate = 1;
}
//用的是和条件等待时同一把锁,不会造成死锁
pthread_mutex_lock(&pool->mutex);
//条件广播释放所有条件等待
pthread_cond_broadcast(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
pool->workers = NULL;
pool->tasks = NULL;
return 0;
}
销毁前,我们要将线程终止标志(任务结束标志)置位并广播释放所有条件等待,这样每个线程拿到锁的时候,就不会阻塞在条件等待,并执行任务结束的操作。
初始化之后我们的线程创建好了,执行者也就位了,那么就得给执行者一些事做了吧,这个事就是创建线程时需要传入的线程回调函数,这个回调函数的工作就是不断地接收新任务,并调用任务执行函数来执行。同时如果检测到需要终止,那么就退出任务接收循环。
worker和锁初始化好了,执行者要做的工作内容也分配好了,还有任务队列相关的操作我们需要来考虑。
我们对外界,首先得提供一个提交任务的接口吧,别人通过这个接口,就可以吧任务信息传进线程池,线程池再进行处理。提交任务的接口如下:
int nThreadPoolPushTask(ThreadPool *pool, struct nTask *task) {
pthread_mutex_lock(&pool->mutex);
LIST_INSERT(task, pool->tasks);
//唤醒一个等待条件的线程
pthread_cond_signal(&pool->cond);
pthread_mutex_unlock(&pool->mutex);
}
那么要怎么去用,已经可以拿去用了,那么怎么去用呢,完整的代码请到这领取:
线程池