一、 线程池的原理:
线程池,究竟是怎么一回事?其实线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:
先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又处于睡眠状态。
可能你也许会问:为什么要搞得这么麻烦,如果每当客户端有新的请求时,我就创建一个新的线程不就完了?这也许是个不错的方法,因为它能使得你编写代码相对容易一些,但你却忽略了一个重要的问题——性能!
就拿我所在的单位来说,我的单位是一个省级数据大集中的银行网络中心,高峰期每秒的客户端请求并发数超过100,如果为每个客户端请求创建一个新线程的话,那耗费的CPU时间和内存将是惊人的,如果采用一个拥有200个线程的线程池,那将会节约大量的的系统资源,使得更多的CPU时间和内存用来处理实际的商业应用,而不是频繁的线程创建与销毁。
二、 数据库连接池
数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。
一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的 性能低下。 数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。
连接池技术尽可能多地重用了消耗内存地资源,大大节省了内存,提高了服务器地服务效率,能够支持更多的客户服务。通过使用连接池,将大大提高程序运行效率,同时,我们可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。
1) 最小连接数是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费;
2) 最大连接数是连接池能申请的最大连接数,如果数据库连接请求超过此数,后面的数据库连接请求将被加入到等待队列中,这会影响之后的数据库操作。
三、 linux线程池的C语言实现
通常我们使用多线程的方式是,需要时创建一个新的线程,在这个新的线程里执行特定的任务,然后在任务完成后退出。这在一般的应用里已经能够满足我们应用的需要,毕竟我们并不是什么时候都需要创建大量的线程,并在它们执行一个简单的任务后销毁。
但是在一些web、email、database等应用里,比如彩铃,我们的应用在任何时候都要准备应对数目巨大的连接请求,同时,这些请求所要完成的任务却又可能非常的简单,即只占用很少的处理时间。这时,我们的应用有可能处于不停的创建线程并销毁线程的状态。虽说比起进程的创建,线程的创建时间已经大大缩短,但是如果需要频繁的创建线程,并且每个线程所占用的处理时间又非常简短,则线程创建和销毁带给处理器的额外负担也是很可观的。
线程池的作用正是在这种情况下有效的降低频繁创建销毁线程所带来的额外开销。一般来说,线程池都是采用预创建的技术,在应用启动之初便预先创建一定数目的线程。应用在运行的过程中,需要时可以从这些线程所组成的线程池里申请分配一个空闲的线程,来执行一定的任务,任务完成后,并不是将线程销毁,而是将它返还给线程池,由线程池自行管理。如果线程池中预先分配的线程已经全部分配完毕,但此时又有新的任务请求,则线程池会动态的创建新的线程去适应这个请求。当然,有可能,某些时段应用并不需要执行很多的任务,导致了线程池中的线程大多处于空闲的状态,为了节省系统资源,线程池就需要动态的销毁其中的一部分空闲线程。因此,线程池都需要一个管理者,按照一定的要求去动态的维护其中线程的数目。
基于上面的技术,线程池将频繁创建和销毁线程所带来的开销分摊到了每个具体执行的任务上,执行的次数越多,则分摊到每个任务上的开销就越小。
当然,如果线程创建销毁所带来的开销与线程执行任务的开销相比微不足道,可以忽略不计,则线程池并没有使用的必要。比如,FTP、Telnet等应用时。
现在已经可以从网上找到一些线程池的实例,但它们一般都是使用C++、Java等语言实现的,如果我们使用C实现我们的应用的话,使用起来就会遇到麻烦。基于此,利用上面的框架,我使用C实现了一个简单的线程池库,代码可以从sourceforge下载。
为了使这个线程池库使用起来更加方便,我在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项目主页下载参考,当然,这个版本只是针对以前做的一个小项目而写的,还有很多需要考虑的地方,比如在线程已经达到最大数目时的处理等等。希望感兴趣的朋友可以加入完善它。