定义
什么是线程池?简单点说,线程池就是有一堆已经创建好了的线程,初始它们都处于空闲等待状态,当有新的任务需要处理的时候,就从这个池子里面取一个空闲等待的线程来处理该任务,当处理完成了就再次把该线程放回池中,以供后面的任务使用。当池子里的线程全都处于忙碌状态时,线程池中没有可用的空闲等待线程,此时,根据需要选择创建一个新的线程并置入池中,或者通知任务线程池忙,稍后再试。
背景
目前的大多数网络服务器,包括Web服务器、Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短.
传统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务。任务执行完毕后,线程退出,这就是是“即时创建,即 时销毁”的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于 不停的创建线程,销毁线程的状态.
我们将传统方案中的线程执行过程分为三个过程:T1、T2、T3。
T1:线程创建时间
T2:线程执行时间,包括线程的同步等时间
T3:线程销毁时间
那么我们可以看出,线程本身的开销所占的比例为(T1+T3) / (T1+T2+T3)。如果线程执行的时间很短的话,这比开销可能占到20%-50%左右。如果任务执行时间很频繁的话,这笔开销将是不可忽略的。
除此之外,线程池能够减少创建的线程个数。通常线程池所允许的并发线程是有上界的,如果同时需要并发的线程数超过上界,那么一部分线程将会等待。而传统方案中,如果同时请求数目为2000,那么最坏情况下,系统可能需要产生2000个线程。尽管这不是一个很大的数目,但是也有部分机器可能达不到这种要求。
因此线程池的出现正是着眼于减少线程池本身带来的开销。线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程(N1),放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N1个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小,不过我们另外可能需要考虑进去线程之间同步所带来的开销。
构建线程池框架
一般线程池都必须具备下面几个组成部分:
线程池管理器:用于创建并管理线程池
工作线程: 线程池中实际执行的线程
任务接口: 尽管线程池大多数情况下是用来支持网络服务器,但是我们将线程执行的任务抽象出来,形成任务接口,从而使线程池与具体的任务无关。
任务队列:线程池的概念具体到实现则可能是队列,链表之类的数据结构,其中保存执行线程。
我们实现的通用线程池框架由五个重要部分组成thread_manager,thread_pool,thread,base_job,work_thread,除此之外框架中还包括线程同步使用的类mutex和condition。
base_job是所有的任务的基类,其提供一个接口run,所有的任务类都必须从该类继承,同时实现run方法。该方法中实现具体的任务逻辑。
thread是Linux中线程的包装,其封装了Linux线程最经常使用的属性和方法,它也是一个抽象类,是所有线程类的基类,具有一个接口run。
work_thread是实际被调度和执行的线程类,其从thread继承而来,实现了thread中的run方法。
thread_pool是线程池类,其负责保存线程,释放线程以及调度线程。
thread_manager是线程池与用户的直接接口,其屏蔽了内部的具体实现。
mutex用于线程之间的互斥。
condition则是条件变量的封装,用于线程之间的同步。
thread_manager直接跟客户端打交道,其接受需要创建的线程初始个数,并接受客户端提交的任务。这儿的任务是具体的非抽象的任务。thread_manager的内部实际上调用的都是thread_poll的相关操作。thread_pool创建具体的线程,并把客户端提交的任务分发给work_thread,work_thread实际执行具体的任务。
理解系统组件
下面我们分开来了解系统中的各个组件。
thread_manager
thread_manager的功能非常简单,其提供最简单的方法,其类定义如下:
class thread_manager
{
public:
thread_manager();
thread_manager(int num);
virtual ~thread_manager();
void set_parallel_num(int num);
void run(base_job* job, void* job_data);
void terminate_all(void);
private:
thread_poll* pool_;
int thread_num_;
};
其中pool指向实际的线程池;thread_num_是初始创建时候允许创建的并发的线程个数。另外run和terminate_all方法也非常简单,只是简单的调用thread_poll的一些相关方法而已。
thread
thread 类实现了对Linux中线程操作的封装,它是所有线程的基类,也是一个抽象类,提供了一个抽象接口run,所有的thread都必须实现该run方法。thread的定义如下所示:
class thread
{
public:
thread();
thread(bool create_suspended, bool detach);
virtual ~thread();
virtual void run(void) = 0;
bool terminate(void); //Terminate the thread
bool start(void); //Start to execute the thread
void exit(void);
bool wake_up(void);
void set_thread_state(thread_state state);
thread_state get_thread_state(void);
void set_thread_name(char* thrname);
char* get_thread_name(void);
int get_thread_id(void);
bool set_priority(int priority);
int get_priority(void);
int get_concurrency(void);
void set_concurrency(int num);
int get_last_error(void);
bool detach(void);
bool join(void);
bool yield(void);
int self(void);
private:
int error_code_;
semaphore thread_semaphore; //the inner semaphore, which is used to realize
unsigned long thread_id_;
bool detach_; //The thread is detached
bool create_suspended; //if suspend after creating
char* thread_name_;
thread_state thread_state_; //the state of the thread
protected:
void set_error_code(int error_code);
static void* thread_function(void*);
};
线程的状态可以分为四种,空闲、忙碌、挂起、终止(包括正常退出和非正常退出)。由于目前Linux线程库不支持挂起操作,因此,我们的此处的挂起操作类似于暂停。如果线程创建后不想立即执行任务,那么我们可以将其“暂停”,如果需要运行,则唤醒。有一点必须注意的是,一旦线程开始执行任务,将不能被挂起,其将一直执行任务至完毕。
线程类的相关操作均十分简单。线程的执行入口是从start()函数开始,其将调用函数thread_function,thread_function再调用实际的run函数,执行实际的任务。
thread_poll
thread_pool是线程的承载容器,一般可以将其实现为堆栈、单向队列或者双向队列。在我们的系统中我们使用vector 对线程进行保存。thread_poll的实现代码如下:
class thread_poll
{
friend class work_thread;
public:
thread_pool();
thread_pool(int init_num);
virtual ~thread_pool();
void set_max_num(int max_num);
int get_max_num(void);
void set_avail_low_num(int min_num);
int get_avail_low_num(void);
void set_avail_high_num(int high_num);
int get_avail_high_num(void);
int get_actual_avail_num(void);
int get_all_num(void);
int get_busy_num(void);
void set_init_num(int init_num);
int get_init_num(void);
void terminate_all(void);
void run(base_job* job, void* job_data);
public:
mutex mutex_busy_; //when visit busy list,use mutex_busy_ to lock and unlock
mutex mutex_idle_; //when visit idle list,use mutex_idle_ to lock and unlock
mutex mutex_job_; //when visit job list,use mutex_job_ to lock and unlock
mutex mutex_var_;
condition cond_busy_; //cond_busy_ is used to sync busy thread list
condition cond_idle_; //cond_idle_ is used to sync idle thread list
condition cond_job_; //cond_job_ is used to sync job list
condition cond_max_num_;
vector<work_thread* > thread_list_;
vector<work_thread* > busy_list_; //busy List
vector<work_thread* > idle_list_; //Idle List
protected:
work_thread* get_idle_thread(void);
void append_to_idle_list(work_thread* job_thread);
void move_to_busy_list(work_thread* idle_thread);
void move_to_idle_list(work_thread* busy_thread);
void delete_idle_thread(int num);
void create_idle_thread(int num);
private:
unsigned int max_num_; //the max thread num that can create at the same time
unsigned int avail_low_; //The min num of idle thread that should kept
unsigned int avail_high_; //The max num of idle thread that kept at the same time
unsigned int avail_num_; //the normal thread num of idle num;
unsigned int init_num_; //Normal thread num;
};
在thread_pool中存在两个链表,一个是空闲链表,一个是忙碌链表。idle链表中存放所有的空闲进程,当线程执行任务时候,其状态变为忙碌状态,同时从空闲链表中删除,并移至忙碌链表中。在thread_pool的构造函数中,我们将执行下面的代码:
for(int i=0; i < init_num_; i++)
{
work_thread* thread = new work_thread();
Append_to_idle_list(thread);
thread->set_thread_pool(this);
Thread->start(); //begin the thread,the thread wait for job
}
在该代码中,我们将创建init_num_个线程,创建之后即调用append_to_idle_list放入idle链表中,由于目前没有任务分发给这些线程,因此线程执行start后将自己挂起。
事实上,线程池中容纳的线程数目并不是一成不变的,其会根据执行负载进行自动伸缩。为此在thread_pool中设定四个变量:
init_num_:初始创建时线程池中的线程的个数。
max_num_:当前线程池中所允许并发存在的线程的最大数目。
avail_low_:当前线程池中所允许存在的空闲线程的最小数目,如果空闲数目低于该值,表明负载可能过重,此时有必要增加空闲线程池的数目。实现中我们总是将线程调整为init_num_个。
avail_high_:当前线程池中所允许的空闲的线程的最大数目,如果空闲数目高于该值,表明当前负载可能较轻,此时将删除多余的空闲线程,删除后调整数也为init_num_个。
avail_num_:目前线程池中实际存在的线程的个数,其值介于avail_high_和avail_low_之间。如果线程的个数始终维持在avail_high_和avail_low_之间,则线程既不需要创建,也不需要删除,保持平衡状态。因此如何设定avail_low_和avail_high的值,使得线程池最大可能的保持平衡态,是线程池设计必须考虑的问题。
线程池在接受到新的任务之后,线程池首先要检查是否有足够的空闲池可用。检查分为三个步骤:
(1)检查当前处于忙碌状态的线程是否达到了设定的最大值max_num_,如果达到了,表明目前没有空闲线程可用,而且也不能创建新的线程,因此必须等待直到有线程执行完毕返回到空闲队列中。
(2)如果当前的空闲线程数目小于我们设定的最小的空闲数目avail_low_,则我们必须创建新的线程,默认情况下,创建后的线程数目应该为init_num_,因此创建的线程数目应该为( 当前空闲线程数与init_num_);但是有一种特殊情况必须考虑,就是现有的线程总数加上创建后的线程数可能超过max_num_,因此我们必须对线程的创建区别对待。
(3)调用get_idle_thread()方法查找空闲线程。如果当前没有空闲线程,则挂起;否则将任务指派给该线程,同时将其移入忙碌队列。
当线程执行完毕后,其会调用move_to_idle_list方法移入空闲链表中,其中还调用cond_idle.signal()方法,唤醒get_idle_thread()中可能阻塞的线程。
base_job
base_job类相对简单,其封装了任务的基本的属性和方法,其中最重要的是Run方法,代码如下:
class base_job
{
public:
base_job( void );
virtual ~base_job();
virtual void run ( void *ptr ) = 0;
int get_job_no(void) ;
void set_job_lno(int job_no);
char* get_job_name(void);
void set_job_name(char* job_name);
thread *get_work_thread(void);
void set_work_thread ( thread *work_thread_ptr );
private:
int job_no_; //The num was assigned to the job
char* job_name_; //The job name
thread *work_thread_ptr; //The thread associated with the job
};
结束语
线程池适合场合
事 实上,线程池并不是万能的。它有其特定的使用场合。线程池致力于减少线程本身的开销对应用所产生的影响,这是有前提的,前提就是线程本身开销与线程执行任 务相比不可忽略。如果线程本身的开销相对于线程任务执行开销而言是可以忽略不计的,那么此时线程池所带来的好处是不明显的,比如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,那么此时,我们采用线程池未必是理想的方法,我们可以选择“即时创建,即时销毁”的策略。
总之线程池通常适合下面的几个场合:
(1) 单位时间内处理任务频繁而且任务处理时间短
(2) 对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。
(3) 必须经常面对高突发性事件,比如Web服务器,如果有足球转播,则服务器将产生巨大的冲击。此时如果采取传统方法,则必须不停的大量产生线程,销毁线程。此时采用动态线程池可以避免这种情况的发生。