一个简单线程池的实现
一、什么是线程池?
线程池的官方定义:
线程池是一种成熟的线程使用模式。实现由领导者与跟随者模式,半同步半异步模式。线程池的伸缩性对性能有较大的影响。创建太多线程,将会浪费一定的资源,有些线程池未被充分使用。销毁太多线程,将导致之后浪费时间再次创建它们。创建线程太慢,将会导致长时间的等待,性能变差。销毁线程池太慢,导致其他线程资源饥饿。
二、为什么用线程池?
1. 创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率,而线程池能有效的减少线程的创建和销毁次数。
2. 如果线程的并发数量过多,线程之间抢占所属进程资源的情况就很严重,很容易导致阻塞,而使用线程池能有效的控制线程的最大并发数,有效避免阻塞问题。
三、什么时候用线程池?
1. 单个任务处理的时间比较短
2. 需要处理的任务数量大
四、什么时候不适宜用线程池?
1. 线程执行需要很长时间,也就是单个的任务非常耗时
2. 需要为线程指定详细的优先级,我们用的线程池都是基于任务队列来排队,先来先执行
3. 在执行过程中需要对线程进行操作,比如睡眠,挂起等,因为线程池本身就是为突然大量爆发的短任务而设计的,如果要使线程睡眠、挂起的话,是与其设计思想相悖的!
五、说了这么多,心动不如行动,我们使用C语言在Linux平台下实现一个简单的线程池
1.首先我们要控制线程池,去为我们执行任务,我们就必须知道一些信息:
(1).当前总共有多少个线程?
(2).当前有多少个空闲的线程可供使用?
(3).线程总数的上限是多少?
(4).我们的任务是通过一个任务队列来管理的,那么控制线程池的执行,也需要直到当前任务队列的头和尾在哪,我需要通过它来知道当前有没有任务,和从队列中取任务
(5).我们要知道什么时候要销毁线程池,所以也要设置一个销毁标记
(6).因为是在多线程下工作,所以需要使用互斥量和条件变量来协调各个线程和保证线程安全
(7).因为要创建任务队列,所以也需要定义任务结点结构
2.知道了上面这些要求,那么我们设计出来的线程池控制结构应该是这样的:
//任务结点结构
typedef struct task
{
void *(*run)(void *arg); //任务回调函数
void *arg; //任务回调函数的参数
struct task *next; //下一个任务结点
}task_t;
//封装互斥量和条件变量为一个condition_t
typedef struct
{
pthread_mutex_t mutex; //互斥量
pthread_cond_t cond; //条件变量
}condition_t;
//线程池结构
typedef struct
{
condition_t ready; //同步、互斥条件
task_t *first; //任务队列头结点指针
task_t *last; //任务队列尾结点指针
int counter; //当前线程总数
int idle; //当前空闲线程数
int quit; //销毁线程池标志
int max_threads; //线程池允许最大线程数
}threadpool_t;
3.对互斥量和条件变量的操作,我们为了方便也要进行一次封装:
condition.h文件:
#ifndef _CONDITION_H
#include <pthread.h>
//封装互斥量和条件变量为一个condition_t
typedef struct
{
pthread_mutex_t mutex; //互斥量
pthread_cond_t cond; //条件变量
}condition_t;
//同步、互斥量的初始化
void condition_init(condition_t *cond);
//互斥量加锁
void condition_lock(condition_t *cond);
//互斥量解锁
void condition_unlock(condition_t *cond);
//条件变量阻塞等待
int condition_wait(condition_t *cond);
//条件变量指定超时时间等待
int condition_timewait(condition_t *cond, const struct timespec *abstime);
//给一个等待中的条件变量发送唤醒信号
void condition_signal(condition_t *cond);
//广播发送给所有等待中的条件变量唤醒信号
void condition_broadcast(condition_t *cond);
//销毁互斥量和条件变量
void condition_destroy(condition_t *cond);
#define _CONDITION_H
#endif
condition.c文件:
#include "condition.h"
//同步、互斥量的初始化
void condition_init(condition_t *cond)
{
pthread_mutex_init(&cond->mutex, NULL);
pthread_cond_init(&cond->cond, NULL);
}
//互斥量加锁
void condition_lock(condition_t *cond)
{
pthread_mutex_lock(&cond->mutex);
}
//互斥量解锁
void condition_unlock(condition_t *cond)
{
pthread_mutex_unlock(&cond->mutex);
}
//条件变量阻塞等待
int condition_wait(condition_t *cond)
{
return pthread_cond_wait(&cond->cond, &cond->mutex);
}
//条件变量指定超时时间等待
int condition_timedwait(condition_t *cond, const struct timespec *abstime)
{
return pthread_cond_timedwait(&cond->cond, &cond->mutex, abstime);
}
//给一个等待中的条件变量发送唤醒信号
void condition_signal(condition_t *cond)
{
pthread_cond_signal(&cond->cond);
}
//广播发送给所有等待中的条件变量唤醒信号
void condition_broadcast(condition_t *cond)
{
pthread_cond_broadcast(&cond->cond);
}
//销毁互斥量和条件变量
void condition_destroy(condition_t *cond)
{
pthread_mutex_destroy(&cond->mutex);
pthread_cond_destroy(&cond->cond);
}
4.操作线程池的函数进行封装,以便其他人调用
threadpool.h文件:
#ifndef _THREADPOOL_H
#include "condition.h"
//任务结点结构
typedef struct task
{
void *(*run)(void *arg); //任务回调函数
void *arg; //任务回调函数的参数
struct task *next; //下一个任务结点
}task_t;
//线程池结构
typedef struct
{
condition_t ready; //同步、互斥条件
task_t *first; //任务队列头结点指针
task_t *last; //任务队列尾结点指针
int counter; //当前线程总数
int idle; //当前空闲线程数
int quit; //销毁线程池标志
int max_threads; //线程池允许最大线程数
}threadpool_t;
//初始化线程池
//@threadpool : 线程池结构指针
//@max_threads : 指定的线程池中最大线程数
void threadpool_init(threadpool_t *threadpool, int max_threads);
//向线程池的任务队列中添加任务
//@threadpool : 线程池结构指针
//@run : 任务回调函数
//@arg : 任务回调函数的参数
void threadpool_add_task(threadpool_t *threadpool, void *(*run)(void *), void *arg);
//销毁线程池
//@threadpool : 线程池结构指针
void threadpool_destroy(threadpool_t *threadpool);
#define _THREADPOOL_H
#endif
threadpool.c文件:
#include <malloc.h>
#include <errno.h>
#include "threadpool.h"
//初始化线程池
//@threadpool : 线程池结构指针
//@max_threads : 指定的线程池中最大线程数
void threadpool_init(threadpool_t *threadpool, int max_threads)
{
//初始化同步、条件变量
condition_init(&(threadpool->ready));
//设置任务队列头、尾指针
threadpool->first = NULL;
threadpool->last = NULL;
//初始化当前线程总数和空闲线程数为0
threadpool->counter = 0;
threadpool->idle = 0;
//设置线程池最大线程数
threadpool->max_threads = max_threads;
//销毁线程池标志设置为0(不销毁), 为1则销毁
threadpool->quit = 0;
}
//线程处理函数
void *route(void *arg)
{
//分离线程
pthread_detach(pthread_self());
//获取线程池结构指针
threadpool_t *threadpool = (threadpool_t *)arg;
//超时标志
int timeout = 0;
while(1)
{
//上锁
condition_lock(&threadpool->ready);
//超时标志重置
timeout = 0;
//当线程创建好,但任务尚未执行时,或者上次任务执行完毕之后再次进入循环,该线程属于空闲线程,所以空闲线程数+1
threadpool->idle++;
//当任务队列为空并且还没有开始销毁线程时,线程进入等待状态
while(threadpool->first == NULL && threadpool->quit == 0)
{
//获取系统当前时间
struct timespec wait_time;
clock_gettime(CLOCK_REALTIME, &wait_time);
//超时等待时间为3秒钟, 如果3秒内没有被唤醒(当新任务来时,会发送唤醒信号),则返回。
wait_time.tv_sec += 3;
int ret = condition_timedwait(&threadpool->ready, &wait_time);
if(ret == ETIMEDOUT)
{
printf("%#x thread timeout!\n", (int)pthread_self());
//设置超时标志
timeout = 1;
break;
}
}
//程序能走到这里,有三种情况: 1.等待超时; 2.任务队列不为空 3.被销毁线程池时发送的广播信号唤醒
//而无论哪种情况,空闲线程都应该 - 1
threadpool->idle--;
//第一种情况: 等待超时而且当前无任务可执行,则销毁该线程
if(timeout == 1 && threadpool->first == NULL)
{
threadpool->counter--;
condition_unlock(&threadpool->ready);
break;
}
//第二种情况: 任务队列不为空, 则执行任务
if(threadpool->first != NULL)
{
//取出队头任务
task_t *task = threadpool->first;
threadpool->first = threadpool->first->next;
//为了避免任务执行时间过长,别的线程获取不到锁,所以先解锁后加锁,绕过任务执行函数
condition_unlock(&threadpool->ready); //解锁
task->run(task->arg); //执行任务
condition_lock(&threadpool->ready); //加锁
//任务执行完毕释放任务结点
free(task);
}
//第三种情况: 下达销毁线程池命令, 且当前无任务可执行,则销毁线程
if(threadpool->quit == 1 && threadpool->first == NULL)
{
threadpool->counter--;
//说明是线程池最后一个线程, 应该发送一个唤醒信号让销毁线程池的函数继续往下执行
if(threadpool->counter == 0)
{
condition_signal(&threadpool->ready);
}
//解锁
condition_unlock(&threadpool->ready);
break;
}
//这里的解锁主要是为任务执行完毕后的解锁
condition_unlock(&threadpool->ready);
}
}
//向线程池的任务队列中添加任务
//@threadpool : 线程池结构指针
//@run : 任务回调函数
//@arg : 任务回调函数的参数
void threadpool_add_task(threadpool_t *threadpool, void *(*run)(void *), void *arg)
{
//上锁(对队列进行增删查改需要互斥访问)
condition_lock(&threadpool->ready);
//构造新任务结点
task_t *new_task = malloc(sizeof(task_t));
new_task->run = run;
new_task->arg = arg;
new_task->next = NULL;
//如果任务队列为空
if(threadpool->first == NULL)
{
threadpool->first = new_task;
}
else
{//如果任务队列不为空
threadpool->last->next = new_task;
}
//修正任务队列尾指针
threadpool->last = new_task;
//如果有空闲线程,则说明可能有空闲线程在等待执行任务
if(threadpool->idle > 0)
{
//唤醒一个在等待状态的空闲线程
condition_signal(&threadpool->ready);
}
//没有空闲线程, 而且当前总线程数小于线程池最大线程个数,则要创建一个新线程
else if(threadpool->counter < threadpool->max_threads)
{
pthread_t tid;
//创建出新线程并将线程池结构指针作为线程处理函数的参数
pthread_create(&tid, NULL, route, threadpool);
//当前线程池总线程数数加1
threadpool->counter++;
}
//解锁
condition_unlock(&threadpool->ready);
}
//销毁线程池
//@threadpool : 线程池结构指针
void threadpool_destroy(threadpool_t *threadpool)
{
//说明有线程已经在处理销毁线程池了,其他线程就不能进去了
if(threadpool->quit == 1)
{
return;
}
//上锁
condition_lock(&threadpool->ready);
threadpool->quit = 1; //标志着已经开始销毁线程池了
//如果有空闲线程, 则说明有线程处于等待状态,则要将它们全部唤醒,让它们自行销毁
if(threadpool->idle > 0)
{
condition_broadcast(&threadpool->ready);
}
else
{//没有空闲线程,则等待所有正在执行任务的线程执行完自己的任务后自行销毁
while(threadpool->counter > 0)
{
//阻塞等待所有正在执行任务的线程执行完毕,当最后一个线程执行完毕时,
//会给发送一个唤醒信号给这里,让销毁线程也退出
condition_wait(&threadpool->ready);
}
}
//销毁互斥、条件变量
condition_destroy(&threadpool->ready);
//解锁
condition_unlock(&threadpool->ready);
}
好,我们这就写好了一个线程池,我们写一个测试程序来跑一下,看是否正确:
main.c文件:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "threadpool.h"
void *mytask(void *arg)
{
printf("thread %#x working on %d\n", pthread_self(), (int)arg);
sleep(1);
}
int main(void)
{
threadpool_t pool;
//初始化线程池,规定其线程最大数为3
threadpool_init(&pool, 3);
int i;
for(i = 0; i < 10; i++)
{
threadpool_add_task(&pool, mytask, (void *)i);
}
sleep(10);
printf("总线程数为%d\n", pool.counter);
printf("空闲线程数为%d\n", pool.idle);
threadpool_destroy(&pool);
return 0;
}