通常我们使用多线程的方式是,需要时创建一个新的线程,在这个新的线程里执行特定的任务,然后在任务完成后退出。这在一般的应用里已经能够满足我们应用的需要,毕竟我们并不是什么时候都需要创建大量的线程,并在它们执行一个简单的任务后销毁。
但是在一些web、email、database等应用里,比如彩铃,我们的应用在任何时候都要准备应对数目巨大的连接请求,同时,这些请求所要完成的任务却又可能非常的简单,即只占用很少的处理时间。这时,我们的应用有可能处于不停的创建线程并销毁线程的状态。虽说比起进程的创建,线程的创建时间已经大大缩短,但是如果需要频繁的创建线程,并且每个线程所占用的处理时间又非常简短,则线程创建和销毁带给处理器的额外负担也是很可观的。
线程池的作用正是在这种情况下有效的降低频繁创建销毁线程所带来的额外开销。一般来说,线程池都是采用预创建的技术,在应用启动之初便预先创建一定数目的线程。应用在运行的过程中,需要时可以从这些线程所组成的线程池里申请分配一个空闲的线程,来执行一定的任务,任务完成后,并不是将线程销毁,而是将它返还给线程池,由线程池自行管理。如果线程池中预先分配的线程已经全部分配完毕,但此时又有新的任务请求,则线程池会动态的创建新的线程去适应这个请求。当然,有可能,某些时段应用并不需要执行很多的任务,导致了线程池中的线程大多处于空闲的状态,为了节省系统资源,线程池就需要动态的销毁其中的一部分空闲线程。因此,线程池都需要一个管理者,按照一定的要求去动态的维护其中线程的数目。
基于上面的技术,线程池将频繁创建和销毁线程所带来的开销分摊到了每个具体执行的任务上,执行的次数越多,则分摊到每个任务上的开销就越小。
当然,如果线程创建销毁所带来的开销与线程执行任务的开销相比微不足道,可以忽略不计,则线程池并没有使用的必要。比如,FTP、Telnet等应用时。
现在已经可以从网上找到一些线程池的实例,但它们一般都是使用C++、Java等语言实现的,如果我们使用C实现我们的应用的话,使用起来就会遇到麻烦。基于此,利用上面的框架,我使用C实现了一个简单的线程池库,代码可以从 https://sourceforge.net/projects/libthreadpool 下载。
为了使这个线程池库使用起来更加方便,我在C实现中加入了一些OO的思想,与Objective-C不同,它仅仅是使用struct模拟了C++中的类,在linux的源代码和一些应用,比如xine里,我们可以看到这种使用方式。
在这个库里,与用户有关的接口主要有:
typedef struct tp_work_desc_s tp_work_desc;
typedef struct tp_work_s tp_work;
typedef struct tp_thread_info_s tp_thread_info;
typedef struct tp_thread_pool_s tp_thread_pool;
//thread parm
struct tp_work_desc_s{
……
};
//base thread struct
struct tp_work_s{
//main process function. user interface
void (*process_job)(tp_work *this, tp_work_desc *job);
};
tp_thread_pool *creat_thread_pool(int min_num, int max_num);
tp_work_desc_s表示应用线程执行任务时所需要的一些信息,会被当作线程的参数传递给每个线程,依据应用的不同而不同,需要用户定义结构的内容。tp_work_s就是我们希望线程执行的任务了。当我们申请分配一个新的线程时,首先要明确的指定这两个结构,即该线程完成什么任务,并且完成这个任务需要哪些额外的信息。接口函数creat_thread_pool用来创建一个线程池的实例,使用时需要指定该线程池实例所能容纳的最小线程数min_num和最大线程数max_num。最小线程数即线程池创建时预创建的线程数目,这个数目的大小也直接影响了线程池所能起到的效果,如果指定的太小,线程池中预创建的线程很快就将分配完毕并需要创建新的线程来适应不断的请求,如果指定的太大,则将可能会有大量的空闲线程。我们需要根据自己应用的实际需要进行指定。描述线程池的结构如下:
//main thread pool struct
struct tp_thread_pool_s{
TPBOOL (*init)(tp_thread_pool *this);
void (*close)(tp_thread_pool *this);
void (*process_job)(tp_thread_pool *this, tp_work *worker, tp_work_desc *job);
int (*get_thread_by_id)(tp_thread_pool *this, int id);
TPBOOL (*add_thread)(tp_thread_pool *this);
TPBOOL (*delete_thread)(tp_thread_pool *this);
int (*get_tp_status)(tp_thread_pool *this);
int min_th_num; //min thread number in the pool
int cur_th_num; //current thread number in the pool
int max_th_num; //max thread number in the pool
pthread_mutex_t tp_lock;
pthread_t manage_thread_id; //manage thread id num
tp_thread_info *thread_info; //work thread relative thread info
};
结构tp_thread_info_s描述了各个线程id、是否空闲、执行的任务等信息,用户并不需要关心它。
//thread info
struct tp_thread_info_s{
pthread_t thread_id; //thread id num
TPBOOL is_busy; //thread status:true-busy;flase-idle
pthread_cond_t thread_cond;
pthread_mutex_t thread_lock;
tp_work *th_work;
tp_work_desc *th_job;
};
tp_thread_pool_s结构包含了有关线程池操作的接口和变量。在使用creat_thread_pool返回一个线程池实例之后,首先要使用明确使用init接口对它进行初始化。在这个初始化过程中,线程池会预创建指定的最小线程数目的线程,它们都处于阻塞状态,并不损耗CPU,但是会占用一定的内存空间。同时init也会创建一个线程池的管理线程,这个线程会在线程池的运行周期内一直执行,它将定时的查看分析线程池的状态,如果线程池中空闲的线程过多,它会删除部分空闲的线程,当然它并不会使所有线程的数目小于指定的最小线程数。
在已经创建并初始化了线程池之后,我们就可以指定tp_work_desc_s和tp_work_s结构,并使用线程池的process_job接口来执行它们。这些就是我们使用这个线程池时所需要了解的所有东西。如果不再需要线程池,可以使用close接口销毁它。
至于内部实现的细节,可以从前面给出的 sourceforge项目主页下载参考,当然,这个版本只是针对以前做的一个小项目而写的,还有很多需要考虑的地方,比如在线程已经达到最大数目时的处理等等。希望感兴趣的朋友可以加入完善它。