为什么需要线程池(thread pool)呢?
我们知道应用程序创建一个对象,然后销毁对象是很耗费资源的。创建线程,销毁线程,也是如此。因此,我们就预先生成一些有限数量线程,另一方面当有任务做的时候把任务挂队列上。
线程:不停的去看有没有还未完成的任务。
有:就去完成任务;没有:休息,等通知。
1.整体框架的搭建,即主进程(main)要做的事情
1.1 初始化一个线程池
bool init_pool(thread_pool *pool, unsigned int threads_num);
thread_pool:自定义的一个描述池子的数据结构;
初始化threads_num的干活的线程,这些线程都做同样的事情:取任务,完成任务;
初始化一些必要的用于管理池子的必要资源:锁、条件变量。
1.2 添加任务
bool add_task(thread_pool *pool, void *(*do_task)(void *arg), (void *)arg);
将任务do_task挂到一个任务队列中去,并且通知线程。
1.3 销毁线程池
bool destroy_pool(thread_pool *pool);
通知干活的线程可以退出了,通过设置一个标志,如shutdown;
等待干活的线程回来;
销毁一些资源:锁,条件变量。
接下来就细分每一步操作了
·······························································································································································
同时有一些值得注意的点:
(1)条件变量pool->cond:在线程的routine函数中,某些情况下,线程会进入到休息状态,需要等待某个条件出来唤醒它。
谁来唤醒它:
父进程退出---------->destroy_pool,并记得唤醒所有线程,并使用 pthread_con_broadcast。
来了新任务 --------->add_task,唤醒一个等在cond的线程上 pthread_cond_signal。
(2)互斥锁pool->lock:保护pool->waiting_tasks,pool->task_list。
至于怎么判别需要保护:公共的变量可能存在并发执行的情况时需要。
我们的项目中,需要加锁的函数涉及:routine, add_task。
在routine这个函数中,线程是有可能退出(调用了pthread_exit),产生带锁退出的情况。
那么,解决方法:
线程在pthread_exit前记得把锁解开;
或者,注册一个清理函数,pthread_cleanup_push必须与pthread_cleanup_pop成对使用。
·······························································································································································
2.分步解析
2.1 线程应该做的事情
其实就是考虑pthread_create函数的第三个参数该怎么写,假设我们对这个函数取名routine。若干个任务怎么去描述?
可以通过组建任务队列,即链表。要做什么事情,就把要做的动作定义在一个函数里头,传给函数(add_task)里的do_task。(thread_pool结构体的定义放在2.2中)
void handler(void *arg)
{
pthread_mutex_unlock((pthread_mutex_t *)arg);//解锁
}
void *routine(void *arg)//任务执行函数
{
struct task * ptask;
thread_pool *pool = (thread_pool *)arg; //线程池指针通过参数传进来
while(1)
{
//--------LOCK--------------//
pthread_cleanup_push(handler, (void *)&pool->lock);
pthread_mutex_lock(&pool->lock);
//---------------------------//
//没事做,休息,等消息 ---->条件变量
while( (0 == pool->waiting_tasks) && (pool->shutdown != 1))
{
pthread_cond_wait(&pool->cond, &pool->lock);
}
//没事做,父进程要撤,所有线程必须撤
if( (0 == pool->waiting_tasks) && ( 1 == pool->shutdown) )
{
pthread_exit(NULL);
}
//有事做,赶紧去取链表上的任务
ptask = pool->task_list;
pool->task_list = ptask->next;
pool->waiting_tasks--;
//--------UNLOCK--------------//
pthread_mutex_unlock(&pool->lock);
pthread_cleanup_pop(0);
//---------------------------//
(ptask->do_task)(ptask->arg);
free(ptask);
}
}
然后我们开始搭建池子结构,这时可以把它定义为一个结构体。至于为什么要设置一个池子,当然是因为控制任务的数量不能太多,不能一次涌入太多的任务。
2.2 池子结构体(thread_pool)的定义
定义一个常量MAX_WAITING_TASK:表示能接纳的最大等待任务数;
在pool结构中需要一个分量waiting_tasks:来记录当前正在等待的任务数;
在pool结构中还需要一个分量active_threads:来记录当前到底有几个线程在干活;
显然,任务列表task_list是一个很重要的公共资源,父进程和干活的线程都要访问;
为了保护这个重要的人物:task_list;则需要用互斥锁+条件变量。
struct task//任务结点
{
void *(*do_task)(void *arg); //函数指针,指向任务要执行的函数
void *arg; //一个指针,任务执行函数时作业函数的参数传入
struct task *next;
};
typedef struct thread_pool//线程池头结点
{
pthread_mutex_t lock; //互斥锁,用来保护这个"线程池",就是保护这个链表的
pthread_cond_t cond; // 有任务的条件
bool shutdown; //是否退出。
unsigned waiting_tasks; //目前等待执行的任务
unsigned active_threads; //干活的线程数
struct task *task_list;//任务链表,即指向第一个任务结点
pthread_t *tids;//指向线程ID的数组,因为可能会创建多个线程。
}thread_pool;
定义好之后我们就需要初始化池子。
2.3 初始化池子(init_pool)
/* 功能:初始化一个线程池
* 输入:
* pool:线程池指针
* threads_number:线程池中活跃线程的个数
* 返回:成功 true, 失败 false
*/
bool init_pool(thread_pool *pool, unsigned int threads_number)
{
pthread_mutex_init(&pool->lock, NULL);
pthread_cond_init(&pool->cond, NULL);
pool->shutdown = 0;
pool->waiting_tasks = 0;
pool->active_threads = threads_number;
pool->task_list = NULL;
pool->tids = malloc(sizeof(pthread_t) * pool->active_threads);
if( NULL == pool->tids)
{
perror("pool malloc err");
return false;
}
int i, r;
for(i=0; i<pool->active_threads; i++)
{
r = pthread_create(&pool->tids[i],
NULL,
routine,
(void *)pool);//将线程池指针作为参数传给routine,
//routine里面要用pool的信息
if( -1 == r)
{
perror("pthread err");
return false;
}
}
return true;
}
2.4 添加任务到池子中(add_task)
由于池子已经初始化完毕,我们就可以往里面添加任务了。
/* 功能:将任务加到线程池的任务队列中
* 输入:
* pool:线程池指针
* do_task:新任务的函数指针
* arg:传递给do_task函数的参数
* 返回:成功 true, 失败 false
*/
#define MAX_WAITING_TASKS 1000
bool add_task(thread_pool *pool, void *(*do_task)(void *arg), void *arg)
{
struct task *new_task = (struct task*) malloc(sizeof(struct task));
new_task->do_task = do_task;
new_task->arg = arg;
new_task->next = NULL;
if(pool->waiting_tasks >= MAX_WAITING_TASKS )
{
printf("排队的人都太多了,拒绝入园...\n");
free(new_task);
return false;
}
//--------LOCK--------------//
pthread_mutex_lock(&pool->lock);
//---------------------------//
//这里采用尾插法,先来的任务先插入
struct task *p;
p = pool->task_list;
if( NULL == p)
{
pool->task_list = new_task;
}
else
{
while( p->next!=NULL )
{
p = p->next;
}
p->next = new_task;
}
pool->waiting_tasks ++;
//--------UNLOCK--------------//
pthread_mutex_unlock(&pool->lock);
//---------------------------//
pthread_cond_signal(&pool->cond);
return true;
}
2.5 销毁线程池(destroy_pool)
bool destroy_pool(thread_pool *pool)
{
int i;
pool->shutdown = 1;
pthread_cond_broadcast(&pool->cond);
for( i = 0; i<pool->active_threads; i++)
{
pthread_join( pool->tids[i] , NULL);
//free(pool->tids[i]);
}
pthread_cond_destroy(&pool->cond);
pthread_mutex_destroy(&pool->lock);
free(pool);
return true;
}
上述一切完成后我们就可以在main函数(即父进程)里调用线程池了,运用第一大点的所述,步骤大致如下:
<<<<1.初始化线程池 init_pool
<<<<2.加入任务 (自定义)
<<<<3.销毁线程池 destroy_pool