深入研究线程池
一.什么是线程池?
线程池就是以一个或多个线程[循环执行]多个应用逻辑的线程集合.
注意这里用了线程集合的概念是我生造的,目的是为了区分执行一批应用逻辑的多个线程和
线程组的区别.关于线程组的概念请参阅基础部分.
诸如web服务器、数据库服务器、文件服务器和邮件服务器等许多服务器应用都面向处理来自某些远程来源的大量短小的任务。构建服务器应用程序的一个过于简单的模型是:每当一个请求到达就创建一个新的服务对象,然后在新的服务对象中为请求服务。但当有大量请求并发访问时,服务器不断的创建和销毁对象的开销很大。所以提高服务器效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这样就引入了“池”的概念,“池”的概念使得人们可以定制一定量的资源,然后对这些资源进行复用,而不是频繁的创建和销毁。
线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。
一般而言,线程池有以下几个部分:
1.完成主要任务的一个或多个线程.
2.用于调度管理的管理线程.
3.要求执行的任务队列.
那么如果一个线程循环执行一段代码是否是线程池?
如果极端而言,应该算,但实际上循环代码应该算上一个逻辑单元.我们说最最弱化的线程池
应该是循环执行多个逻辑单元.也就是有一批要执行的任务,这些任务被独立为多个不同的执行
单元.比如:
int x = 0;
while(true){
x ++;
}
这就不能说循环中执行多个逻辑单元,因为它只是简单地对循环外部的初始变量执行++操作.
而如果已经有一个队列
ArrayList al = new ArrayList();
for(int i=0;i<10000;i++){
al.add(new AClass());
}
然后在一个线程中执行:
while(al.size() != 0){
AClass a = (AClass)al.remove(0);
a.businessMethod();
}
我们说这个线程就是循环执行多个逻辑单元.可以说这个线程是弱化的线程池.我们习惯上把这些
相对独立的逻辑单元称为任务.
二.为什么要创建线程池?
线程池属于对象池.所有对象池都具有一个非常重要的共性,就是为了最大程度复用对象.那么
线程池的最重要的特征也就是最大程度利用线程.
从编程模型模型上说讲,在处理多任务时,每个任务一个线程是非常好的模型.如果确实可以这么
做我们将可以使用编程模型更清楚,更优化.但是在实际应用中,每个任务一个线程会使用系统限
入"过度切换"和"过度开销"的泥潭.
打个比方,如果可能,生活中每个人一辆房车,上面有休息,娱乐,餐饮等生活措施.而且道路交道永远
不堵车,那是多么美好的梦中王国啊.可是残酷的现实告诉我们,那是不可能的.不仅每个人一辆车
需要无数多的社会资源,而且地球上所能容纳的车辆总数是有限制的.
首先,创建线程本身需要额外(相对于执行任务而必须的资源)的开销.
作业系统在每创建一个线程时,至少需要创建以下资源:
线程内核对象用于对线程上下文的管理.
用户模式执行栈.
内核模式执行栈.
这些资源被线程占有后作业系统和用户都无法使用.
相反的过程,销毁线程需要回收资源,也需要一定开销.
其次,过多的线程将导致过度的切换.
线程切换带来的性能更是不可估量.系统完成线程切换要经过以下过程:
从用户模式切换到内核模式.
将CPU寄存器的值保存到当前线程的内核对象中.
打开一个自旋锁,根据调度策略决定下一个要执行的线程.释放自旋锁,如果要执行的线程不是同一
进程中的线程,还需要切换虚拟内存等进程环境.
将要执行的线程的内核对象的值写到CPU寄存器中.
切换到用户模式执行新线程的执行逻辑.
以上开销对于用户要执行的任务而言,都是额外的.更不可容忍的是,如果用户的任务逻辑都是很小
的单元,而新分配线程和线程切换的开销与任务逻辑需要的开销的比例可能会10:1,100:1,1000:1.
也就是你花了1000$买的衣服只穿了一天!
所以线程池的目的就是为了减少创建和切换线程的额外开销,利用已经的线程多次循环执行多个任
务从而提高系统的处理能力.也就是在"社会主义初级阶段"一件衣服应该尽量多穿一些天数.
线程池的注意事项
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。
(1)线程池大小。多线程应用并非线程越多越好,需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。一般来说,如果代码结构合理的话,线程数目与CPU 数量相适合即可。如果线程运行时可能出现阻塞现象,可相应增加池的大小;如有必要可采用自适应算法来动态调整线程池的大小,以提高CPU 的有效利用率和系统的整体性能。
(2)并发错误。多线程应用要特别注意并发错误,要从逻辑上保证程序的正确性,注意避免死锁现象的发生。
(3)线程泄漏。这是线程池应用中一个严重的问题,当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。
简单线程池的设计
一个典型的线程池,应该包括如下几个部分:
1、线程池管理器(ThreadPool),用于启动、停用,管理线程池
2、工作线程(WorkThread),线程池中的线程
3、请求接口(WorkRequest),创建请求对象,以供工作线程调度任务的执行
4、请求队列(RequestQueue),用于存放和提取请求
5、结果队列(ResultQueue),用于存储请求执行后返回的结果
线程池管理器,通过添加请求的方法(putRequest)向请求队列(RequestQueue)添加请求,这些请求事先需要实现请求接口,即传递工作函数、参数、结果处理函数、以及异常处理函数。之后初始化一定数量的工作线程,这些线程通过轮询的方式不断查看请求队列(RequestQueue),只要有请求存在,则会提取出请求,进行执行。然后,线程池管理器调用方法(poll)查看结果队列(resultQueue)是否有值,如果有值,则取出,调用结果处理函数执行。通过以上讲述,不难发现,这个系统的核心资源在于请求队列和结果队列,工作线程通过轮询requestQueue获得人物,主线程通过查看结果队列,获得执行结果。因此,对这个队列的设计,要实现线程同步,以及一定阻塞和超时机制的设计,以防止因为不断轮询而导致的过多cpu开销。在本文中,将会用python语言实现,python的Queue,就是很好的实现了对线程同步机制。