我okhttp3 设置线程池了一个线程池,为啥他总是跑不到我指定的次数

您当前位置: >
> 线程池(一)
线程池(一)
来源:程序员人生&& 发布时间: 14:42:23 阅读次数:921次
甚么是线程池?为何要使用线程池?
进行开发的时候,我们应当都接触过连接池,为了不每次进行连接的时候都重新新建和烧毁连接,我们可使用1个连接池来保护1些连接,让他们长时间保持1个激活的状态,当系统需要使用使用的时候,就从连接池中拿来1个可用的连接便可,而不是创建新的连接。反之,当我们需要关闭连接的时候,也不是真的关闭连接,而是将这个连接返还给连接池。通过这样的方式,可以节俭很多的创建和烧毁对象的时间。
其实线程池也是类似的概念,线程池中总有那末几个活跃的线程,当你需要的时候可以从线程池里随意拿来1个空闲线程,当完成工作时其实不着急关闭线程,而是返回给线程池,方便其他人使用。
这里我们肯定还有疑问为何我们传统的创建自定义线程有甚么问题?问题就是虽然线程是1种轻量级的工具,但它的创建和关闭都需要花费时间,如我们在程序中随便的创建线程而不加控制其数量,反而会耗尽cpu和内存资源。即使没有outofmemory异常,大量的回收线程也会致使GC停顿的时间延长。所以我们实际中可以优先斟酌使用线程池对线程进行控制和管理,更加有效的公道的使用线程进行提高程序的性能。
jdk对线程池的支持
Jdk提供了1套Executor框架,核心成员以下图:
其中ThreadPoolExecutor是1个线程池,Executors扮演着1个线程工厂的角色,通过Executors类可以获得1个具有特定功能的线程池。通过UML图我们可以看到ThreadPoolExecutor实现Executor接口通过这个接口,任何Runable对象都可以被ThreadPoolExecutor线程池调度。
Executors主要提供以下工厂方法:
static ExecutorService newCachedThreadPool()
static ExecutorService newFixedThreadPool(int nThreads)
static ExecutorService newSingleThreadExecutor()
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
static ScheduledExecutorService newSingleThreadScheduledExecutor()
newCachedThreadPool()方法:该方法返回1个会根据实际情况进行调剂的线程池,线程数量不肯定。但如果有空闲的线程可复用,则优先选择可复用的线程。若当先线程都在工作,同时又有新的任务提交,则会创建新的线程来处理。所有线程在当前任务完成的时候,将返回线程池进行复用。
newFixedThreadPool(int nThreads)方法:该方法返回1个具有固定数量的线程的线程池。该线程池中的线程数量始终不变。当1个任务提交时,若有空闲线程则履行,若没有,会被交给1个任务队列,当有空闲线程的时候,就会处理任务队里的任务。
newScheduleThreadPool(int corePoolSize)方法:该方法会返回1个ScheduledExecutorService对象。ScheduledExecutorService接口在ExecutorService接口的基础上扩大了在给定时间履行某任务的功能,如某个固定时间后开始履行,或周期性的履行某个任务。
newSingleThreadScheduleExecutor();也返回1个ScheduledExecutorService对象,不过线程池只有1个线程。
下面简单演示下newFixedThreadPool(int nThreads)的简单使用:
import java.util.concurrent.ExecutorS
import java.util.concurrent.E
import java.util.concurrent.TimeU
* 简单展现newFixedThreadPool
* @author zdm
public class ThreadPoolDemo {
public static class MyTask implements Runnable {
public void run() {
System.out.println(System.currentTimeMillis() + &:ThreadId:&
+ Thread.currentThread().getId());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(5);
for(int i = 0; i & 10; i++) {
es.execute(new MyTask());
}运行结果:
有运行结果可以知道具有5个线程的线程池的把10个任务分两批完成,前5个和后5个任务恰好相差了1秒,因而可知上面程序符合newFixedThreadPool产的线程池的行动。
有时间计划的任务:
newScheduleThreadPool(int corePoolSize)返回1个ScheduleExecutorService对象,可以根据时间需要对线程进行调度,它的主要方法以下:
ScheduledFuture&?& schedule(Runnable command, long delay, TimeUnit unit)
//创建并履行在给定延迟后启用的1次性操作。
ScheduledFuture&?& scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
//创建并履行1个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始履行,然后在 initialDelay+period 后履行,接着在 initialDelay + 2 * period 后履行,依此类推。
ScheduledFuture&?& scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
//创建并履行1个在给定初始延迟后首次启用的定期操作,随后,在每次履行终止和下1次履行开始之间都存在给定的延迟。与其他线程池不同,ScheduledExecutorService其实不1定会立即安排履行任务,它会在指定的时间,对任务进行调度,起到的计划任务的作用。
这里需要注意的就是scheduleAtFixedRate和scheduleWithFixedDelay这两个方法都是对任务进行周期性的调度,但是又有1点不同。
对FixedRate的方式来讲,任务调度的频率是1定的,它是以上1个任务开始履行的时间为出发点,以后的period时间,调度下1次任务。而FixedDealy则是上1个任务结束后,在经过delay对下1次任务进行调度。
如果还有疑问,我们可以官方的文档对它两的解释:
scheduleAtFixRate(Runnable command, long initialDealy, long period, TimeUnit unit):
Creates and executes a periodic action that becomes enabled first&after the given initial delay, and subsequently with the given& that is executions will commence after&initialDelay &then initialDelay+period, then&initialDelay + 2 * period, and
翻译:创建1个周期性的任务,任务开始于指定的初始延迟,后续的任务依照给定的周期履行:后续第1个任务将会在initialDelay+period,下1个任务将在initialDelay+2period履行。
scheduleWithFixedDelay(Runnable command, long initialDealy, long delay, TimeUnit unit):
Creates and executes a periodic action that becomes enabled first&after the given initial delay, and subsequently with the&given delay between the termination of one execution and the&commencement of the next. &If any execution of the task&encounters an
exception, subsequent executions are suppressed.&Otherwise, the task will only terminate via cancellation or&termination of the executor.
翻译:创建1个周期性的任务,任务开始于指定的初始延迟,后续的任务依照给定的延迟履行,即上1个任务结束的时间到下1个任务开始时间的时间差。
下面可以简单演示下ScheduledExecutorService的scheduleAtFixedRate的方法:
import java.util.concurrent.E
import java.util.concurrent.ScheduledExecutorS
import java.util.concurrent.TimeU
* ScheduledExecutorService的简单示例
* @author zdm
public class ScheduleExecutorServiceDemo {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
ses.scheduleAtFixedRate(new Runnable() {
public void run() {
TimeUnit.SECONDS.sleep(1);
System.out.println(System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}, 0, 2, TimeUnit.SECONDS);
}运行结果:
这个任务履行使用1秒,周期为2秒,也就是2秒履行1次任务,恰好运行结果符合我们的预期目标。
但是这里有1个问题就是如果任务的履行时间大于调度周期的时间会产生怎样办?这里我们将TimeUnit.SECONDS.sleep(5)尝试1下
会发现任务的周期调度变成了5秒~~~
如果采取scheduledWithFixedDelay()调用会依照修改任务履行需要5秒,延迟为2秒,那末任务的实际间隔为7秒。
这里还需要注意1个问题:那就是调度程序其实不会无穷期的延续等待。如果任务本身产生了异常,那末后续的子任务都会停止调用。所以需要对异常进行及时的处理,以保证周期性任务的稳定性。
线程池的内部实现
对核心的那几个线程池,虽然看上去功能各不相同其实内部都是使用了ThreadPoolExecutor实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue&Runnable&());
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue&Runnable&()));
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue&Runnable&());
}从上面我们可以发现它们都是ThreadPoolExecutor的封装,为何功能如此强大?我们可以看1下ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue&Runnable& workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
方法的参数含义以下:
corePoolSize:表示线程中的线程数量。
maximuumPoolSize:表示线程池中最大的线程数量。
keepAliveTime:当线程数量超过corePoolSize时,过剩的空闲线程的存活时间
unit:表示存活时间单位
workQueue:任务队列,被提交但却没有没履行的任务。
threadFactory:线程工厂,用于创建线程,1般用默许的便可。
handler:谢绝策略,当任务太多来不及处理时,如何谢绝。
其中的参数workQueue是1个BlockingQueue&Runnable&接口,它有几种不同功能的子类队列:
先认识1下Blocking:
阻塞队列,顾名思义,首先它是1个队列,而1个队列在数据结构中所起的作用大致以下图所示:
从上图我们可以很清楚看到,通过1个同享的队列,可使得数据由队列的1端输入,从另外1端输出;
经常使用的队列主要有以下两种:(固然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的1种)
  先进先出(FIFO):先插入的队列的元素也最早出队列,类似于排队的功能。从某种程度上来讲这类队列也体现了1种公平性。
  落后先出(LIFO):后插入队列的元素最早出队列,这类队列优先处理最近产生的事件。
&& & &多线程环境中,通过队列可以很容易实现数据同享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现二者之间的数据同享。假定我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据同享给消费者线程,利用队列的方式来传递数据,就能够很方便地解决他们之间的数据同享问题。但如果生产者和消费者在某个时间段内,万1产生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据积累到1定程度的时候,那末生产者必须暂停等待1下(阻塞生产者线程),以便等待消费者线程把积累的数据处理终了,反之亦然。但是,在concurrent包发布之前,在多线程环境下,我们每一个程序员都必须去自己控制这些细节,特别还要统筹效力和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),1旦条件满足,被挂起的线程又会自动被唤醒)
下面两幅图演示了BlockingQueue的两个常见阻塞场景:
如上图所示:当队列中没有数据的情况下,消费者真个所有线程都会被自动阻塞(挂起),直到有数据放入队列。
如上图所示:当队列中填满数据的情况下,生产者真个所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
&这也是我们在多线程环境下,为何需要BlockingQueue的缘由。作为BlockingQueue的使用者,我们不再需要关心甚么时候需要阻塞线程,甚么时候需要唤醒线程,由于这1切BlockingQueue都给你1手包办了。既然BlockingQueue如此神通广大,让我们1起来见识下它的经常使用方法:
BlockingQueue的核心方法:
放入数据:
offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.(本方法不阻塞当前履行方法的线程)
offer(E o, long timeout, TimeUnit unit),可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败。
put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻塞直到BlockingQueue里面有空间再继续.
获得数据:
  poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回
  poll(long timeout, TimeUnit unit):从BlockingQueue取出1个队首的对象,如果在指定时间内,队列1旦有数据可取,则立即返回队列中的数据。否则知道时间超时还没有数据可取,返回失败。
  take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入;&
  drainTo():1次性从BlockingQueue获得所有可用的数据对象(还可以指定获得数据的个数),通过该方法,可以提升获得数据效力;不需要屡次分批加锁或释放锁。
BlockingQueue成员详细介绍
1. ArrayBlockingQueue
& & & 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,保护了1个定长数组,以便缓存队列中的数据对象,这是1个经常使用的阻塞队列,除1个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
  ArrayBlockingQueue在生产者放入数据和消费者获得数据,都是共用同1个锁对象,由此也意味着二者没法真正并行运行,这点特别不同于LinkedBlockingQueue;依照实现原理来分析,ArrayBlockingQueue完全可以采取分离锁,从而实现生产者和消费者操作的完全并行运行。Doug
Lea之所以没这样去做,或许是由于ArrayBlockingQueue的数据写入和获得操作已足够轻巧,以致于引入独立的锁机制,除给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有1个明显的不同的地方在于,前者在插入或删除元素时不会产生或烧毁任何额外的对象实例,而后者则会生成1个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对GC的影响还是存在1定的区分。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是不是采取公平锁,默许采取非公平锁。
LinkedBlockingQueue
&& & &基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也保持着1个数据缓冲队列(该队列由1个链表构成),当生产者往队列中放入1个数据时,队列会从生产者手中获得数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区到达最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉1份数据,生产者线程会被唤醒,反之对消费者这真个处理也基于一样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还由于其对生产者端和消费者端分别采取了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高全部队列的并发性能。
作为开发者,我们需要注意的是,如果构造1个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默许1个类似无穷大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度1旦大于消费者的速度,或许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
ArrayBlockingQueue和LinkedBlockingQueue是两个最普通也是最经常使用的阻塞队列,1般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
DelayQueue
& & DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获得到该元素。DelayQueue是1个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永久不会被阻塞,而只有获得数据的操作(消费者)才会被阻塞。
使用处景:
  DelayQueue使用处景较少,但都相当奇妙,常见的例子比如使用1个DelayQueue来管理1个超时未响应的连接队列。
PriorityBlockingQueue
&& & &基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue其实不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间1长,会终究耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采取的是公平锁。
5. SynchronousQueue
&& & &1种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的终究消费者,而消费者必须亲身去集市找到所要商品的直接生产者,如果1方没有找到适合的目标,那末对不起,大家都在集市等待。相对有缓冲的BlockingQueue来讲,少了1个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在乎经销商终究会将这些产品卖给那些消费者,由于经销商可以库存1部份商品,因此相对直接交易模式,整体来讲采取中间经销商的模式会吞吐量高1些(可以批量买卖);但另外一方面,又由于经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会下降。
  声明1个SynchronousQueue有两种不同的方式,它们之间有着不太1样的行动。公平模式和非公平模式的区分:
  如果采取公平模式:SynchronousQueue会采取公平锁,并配合1个FIFO队列来阻塞过剩的生产者和消费者,从而体系整体的公平策略;
  但如果是非公平模式(SynchronousQueue默许):SynchronousQueue采取非公平锁,同时配合1个LIFO队列来管理过剩的生产者和消费者,而后1种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,便可能有某些生产者或是消费者的数据永久都得不到处理。所以在使用SynchronousQueue时会设置很大的maximumPoolSize,而否则会很容易履行谢绝策略。
  BlockingQueue不光实现了1个完全队列所具有的基本功能,同时在多线程环境下,他还自动管理了多线间的自动等待于唤醒功能,从而使得程序员可以疏忽这些细节,关注更高级的功能。&
由此可以知道,在使用newCachedThreadPool时,当提交的任务过量时,没有空闲的线程,使用SynchronousQueue,它是直接提交任务的队列,从而迫使线程池创建新的线程来处理任务。当任务履行完成,它会被指定的时间内被回收。由于maximumPoolSize=0。所以当有大量任务提交,而任务的处理又不是很快的情况下,会致使系统资源的耗尽。
使用newFixedThreadPool和newSingleThreadExecutor时应当注意无界的LinkedBlockingQueue的增长。
下面给出线程池的核心调度任务的代码:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) & corePoolSize) {
if (addWorker(command, true))
c = ctl.get();
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
else if (!addWorker(command, false))
reject(command);
}代码第5行的workerCountOf()方法获得当前线程池中的线程总数,当线程总数小于corePoolSize时,会将任务通过方法addWorker()直接调度。否则workQueue.offer()进入任务队列,如果进入任务队列失败(有界队列到达上限或使用SynchronousQueue),则会履行17行,将任务提交到线程池,当前线程数到达maximumPoolSize,则提交失败,使用谢绝策略,未到达,则分配线程履行。
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
------分隔线----------------------------
------分隔线----------------------------
积分:4237我的图书馆
什么是线程池?
诸如web服务器、数据库服务器、文件服务器和邮件服务器等许多服务器应用都面向处理来自某些远程来源的大量短小的任务。构建服务器应用程序的一个
过于简单的模型是:每当一个请求到达就创建一个新的服务对象,然后在新的服务对象中为请求服务。但当有大量请求并发访问时,服务器不断的创建和销毁对象的
开销很大。所以提高服务器效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这样就引入了“池”的概念,“池”的
概念使得人们可以定制一定量的资源,然后对这些资源进行复用,而不是频繁的创建和销毁。
线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消
耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运
行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。
线程池的注意事项
虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。在使用线程池时需注意线程池大小与性能的关系,注意并发风险、死锁、资源不足和线程泄漏等问题。
(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,就是很好的实现了对线程同步
Microsoft .NET
提供的线程池,
揭示什么情况下你需要用线程池以及
框架下的线程池是如何实现的,并告诉你如何去使用线程池。
.NET中的线程池
线程池中执行的函数
使用定时器
同步对象的执行
异步I/O操作
监视线程池
有关安全性
果你有在任何编程语言下的多线程编程经验的话,你肯定已经非常熟悉一些典型的范例。通常,多线程编程与基于用户界面的应用联系在一起,它们需要在不影响终
端用户的情况下,执行一些耗时的操作。取出任何一本参考书,打开有关线程这一章:你能找到一个能在你的用户界面中并行执行数学运算的多线程示例吗?
我的目的不是让你扔掉你的书,不要这样做!多线程编程技术使基于用户界面的应用更完美。实际上,
Microsoft .NET
框架支持在任何语言编写的窗口下应用多线程编程技术,允许开发人员设计非常丰富的界面,提供给终端用户一个更好的体验。但是,多线程编程技术不仅仅是为了用户界面的应用,在没有任何用户界面的应用中,一样会出现多个执行流的情况。
我们用一个“硬件商店”的客户
服务器应用系统作为例子。客户端是收银机,服务端是运行在仓库里一台独立的机器上的应用系统。你可以想象一下,服务器没有任何的用户界面,如果不用多线程技术你将如何去实现?
服务端通过通道(
http, sockets, files
等等)接收来自客户端的请求并处理它们,然后发送一个应答到客户端。图
显示了它是如何运作的。
单线程的服务端应用系统
为了让客户端的请求不会遗漏,服务端应用系统实现了某种队列来存放这些请求。图
显示了三个请求同时到达,但只有其中的一个被服务端处理。当服务端开始执行
"Decrease stock of monkey wrench,"
这个请求时,其它两个必须在队列中等待。当第一个执行完成后,接着是第二个,以此类推。这种方法普遍用于许多现有的系统,但是这样做系统的资源利用率很低。假设
decreasing the stock
”请求修改磁盘上的一个文件,而这个文件正在被修改中,
将不会被使用,即使这个请求正处在待处理阶段。这类系统的一个普遍特征就是低
利用时间导致出现很长的响应时间,甚至是在访问压力很大的环境里也这样。
另外一个策略就是在当前的系统中为每一个请求创建不同的线程。当一个新的请求到达之后,服务端为进入的请求创建一个新线程,执行结束时,再销毁它。下图说明了这个过程:
:多线程服务端应用系统
所示的那样。我们有了较高的
用率。即使它已经不再像原来的那样慢了,但创建线和销毁程也不是最恰当的方法。假设线程的执行操作不复杂,由于需要花额外的时间去创建和销毁线程,所以最
终会严重影响系统的响应时间。另外一点就是在压力很大的环境下,这三个线程会给系统带来很多的冲击。多个线程同时执行请求处理将导致
的利用率达到
,而且大多数时间会浪费在上下文切换过程中,甚至会超过处理请求的本身。这类系统的典型特征是大量的访问会导致响应时间呈指数级增长和很高的
使用时间。
一个最优的实现是综合前面两种方案而提出的观点
Thread Pool
),当一个请求达到时,应用系统把置入接收队列,一组的线程从队列提取请求并处理之。这个方案如下图所示:
:启用线程池的服务端应用系统
在这个例子中,我们用了一个含有两个线程的线程
池。当三个请求到达时,它们立刻安排到队列等待被处理,因为两个线程都是空闲的,所以头两个请求开始执行。当其中任何一个请求处理结束后,空闲的线程就会
去提取第三个请求并处理之。在这种场景中,系统不需要为每个请求创建和销毁线程。线程之间能互相利用。而且如果线程池的执行高效的话,它能增加或删除线程
以获得最优的性能。例如当线程池在执行两个请求时,而
的利用率才达到
,这表明执行请求正等待某个事件或者正在做某种
操作。线程池可以发现这种情况,并增加线程的数量以使系统能在同一时间处理更多的请求。相反的,如果
利用率达到
,线程池可以减少线程的数量以获得更多的
时间,而不要浪费在上下文切换上面。
中的线程池
基于上面的例子,在企业级应用系统中有一个高效执行的线程池是至关重要的。
框架的开发环境中已经实现了这个,该系统的核心提供了一个现成可用的最优线程池。
这个线程池不仅对应用程序可用,而且还融合到框架中的多数类中。
建立在同一个池上是一个很重要的功能特性。比如
.NET Remoting
用它来处理来自远程对象的请求。
当一个托管应用程序开始执行时,运行时环境(
)提供一个线程池,它将在代码第一次访问时被创建。这个池与应用程序所在运行的物理进程关联在一起,当你用
框架下的同一进程中运行多个应用程序的功能特性时(称之为应用程序域),这将是一个很重要的细节。在这种情况下,由于它们都使用同样的线程池,一个坏的应用程序会影响进程中的其它应用程序。
你可以通过
System.Threading
名称空间的
Thread Pool
来使用线程池,如果你查看一下这个类,就会发现所有的成员都是静态的,而且没有公开的构造函数。这是有理由这样做的,因为每个进程只有一个线程池,并且我
们不能创建新的。这个限制的目的是为了把所有的异步编程技术都集中到同一个池中。所以我们不能拥有一个通过第三方组建创建的无法管理的线程池。
线程池中执行的函数
ThreadPool.QueueUserWorkItem
方法运行我们在系统线程池上启动一个函数,它的声明如下:
public static bool QueueUserWorkItem (WaitCallback callBack, object state)
第一个参数指明我们将在池中执行的函数,它的声明必须与WaitCallback
代理(delegate)互相匹配:public delegate void WaitCallback (object state);
参数允许任何类型的信息传递到该方法中,它在调用
QueueUserWorkItem
让我们结合这些新概念,看看“硬件商店”的另一个实现。
using System.T
namespace ThreadPoolTest
&& class MainApp
&&&&& static void Main()
&&&&&&&& WaitCallback callB
&&&&&&&& callBack = new WaitCallback(PooledFunc);
&&&&&&&& ThreadPool.QueueUserWorkItem(callBack,
&&&&&&&&&&& "Is there any screw left?");
&&&&&&&& ThreadPool.QueueUserWorkItem(callBack,
&&&&&&&&&&& "How much is a 40W bulb?");
&&&&&&&& ThreadPool.QueueUserWorkItem(callBack,
&&&&&&&&&&& "Decrease stock of monkey wrench");&&
&&&&&&&&&Console.ReadLine();
&&&&& static void PooledFunc(object state)
&&&&&&&& Console.WriteLine("Processing request '{0}'", (string)state);
&&&&&&&& // Simulation of processing time
&&&&&&&& Thread.Sleep(2000);
&&&&&&&& Console.WriteLine("Request processed");
为了简化例子,我们在
类中创建一个静态方法用于处理请求。由于代理的灵活性,我们可以指定任何实例方法去处理请求,只要这些方法的声明与代理相同。在这里范例中,通过调用
Thread.Sleep
,实现延迟两秒以模拟处理时间。
你如果编译和执行这个范例,将会看到下面的输出:
Processing request 'Is there any screw left?'
Processing request 'How much is a 40W bulb?'
Processing request 'Decrease stock of monkey wrench'
Request processed
Request processed
Request processed
注意,所有的请求都被不同的线程并行处理了。
我们可以通过在两个方法中加入如下的代码,以此看到更多的信息。
&// Main method
&& Console.WriteLine("Main thread. Is pool thread: {0}, Hash: {1}",
&&&&&&&&&&& Thread.CurrentThread.IsThreadPoolThread,
&&&&&&&&&&&&Thread.CurrentThread.GetHashCode());
&& // Pool method
&& Console.WriteLine("Processing request '{0}'." +
&&&&&&" Is pool thread: {1}, Hash: {2}",
&&&&& (string)state, Thread.CurrentThread.IsThreadPoolThread,
&&&&&&Thread.CurrentThread.GetHashCode());
我们增加了一个
Thread.CurrentThread.IsThreadPoolThread
的调用。如果目标线程属于线程池,这个属性将返回
。另外,我们还显示了用
GetHashCode
方法从当前线程返回的结果。它是唯一标识当前执行线程的值。现在看一看这个输出结果:
Main thread. Is pool thread: False, Hash: 2
Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 4
Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8
Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 9
Request processed
Request processed
Request processed
你可以看到所有的请求都被系统线程池中的不同线程执行。再次运行这个例子,注意系统
的利用率,如果你没有任何其它应用程序在后台运行的话,它几乎是
。因为系统唯一正在做的是每执行
秒后就挂起的处理。
我们来修改一下这个应用,这次我们不挂起处理请求的线程,相反我们会一直让系统忙,为了做到这点,我们用
Environment.TickCount
. 构建一个每隔两秒就对请求执行一次的循环。
int ticks = Environment.TickC
while(Environment.TickCount - ticks & 2000);
现在打开任务管理器,看一看
的使用率,你将看到应用程序占有了
%的使用率。再看一下我们程序的输出结果:
Processing request 'Is there any screw left?'. Is pool thread: True, Hash: 7
Processing request 'How much is a 40W bulb?'. Is pool thread: True, Hash: 8
Request processed
Processing request 'Decrease stock of monkey wrench '. Is pool thread: True, Hash: 7
Request processed
Request processed
注意第三个请求是在第一个请求处理结束之后执行的,而且线程的号码仍然用原来的
,这个原因是线程池检测到
的使用率已经达到
%,一直等待某个线程空闲。它并不会重新创建一个新的线程,这样就会减少线程间的上下文切换开销,以使总体性能更佳。
使用定时器
假如你曾经开发过
Microsoft Win32
的应用程序,你知道
之一,通过这个函数可以指定的一个窗口接收到来自系统时间周期的
消息。用这个方法遇到的第一个问题是你需要一个窗口去接收消息,所以你不能用在控制台应用程序中。另外,基于消息的实现并不是非常精确,假如你的应用程序正在处理其它消息,情况有可能更糟糕。
的定时器来说,
中一个很重要的改进就是创建不同的线程,该线程阻塞指定的时间,然后通知一个回调函数。这里的定时器不需要
的消息系统,所以这样就更精确,而且还能用于控制台应用程序中。以下代码显示了这个技术的一种实现:
class MainApp
&& static void Main()
&&&&& MyTimer myTimer = new MyTimer(2000);
&&&&& Console.ReadLine();
class MyTimer
&& public MyTimer(int period)
&&&&& m_period =
&&&&& thread = new Thread(new ThreadStart(TimerThread));
&&&&& thread.Start();
&& void TimerThread()
&&&&& Thread.Sleep(m_period);
&&&&& OnTimer();
&& void OnTimer()
&&&&& Console.WriteLine("OnTimer");
这个代码一般用于
应用中。每个定时器创建独立的线程,并且等待指定的时间,然后呼叫回调函数。犹如你看到的那样,这个实现的成本会非常高。如果你的应用程序使用了多个定时器,相对的线程数量也会随着使用定时器的数量而增长。
现在我们有
提供的线程池,我们可以从池中改变请求的等待函数,这样就十分有效,而且会提升系统的性能。我们会遇到两个问题:
假如线程池已满(所有的线程都在运行中),那么这个请求排到队列中等待,而且定时器不在精确。
假如创建了多个定时器,线程池会因为等待它们时间片失效而非常忙。
为了避免这些问题,
框架的线程池提供了独立于时间的请求。用了这个函数,我们可以不用任何线程就可以拥有成千上万个定时器,一旦时间片失效,这时,线程池将会处理这些请求。
这些特色出现在两个不同的类中:
System.Threading.Timer
&&&&&&&&&&&&&&&&&&
定时器的简单版本,它运行开发人员向线程池中的定期执行的程序指定一个代理(
System.Timers.Timer
System.Threading.Timer
的组件版本,允许开发人员把它拖放到一个窗口表单(
)中,可以把一个事件作为执行的函数。
这非常有助于理解上述两个类与另外一个称为
System.Windows.Forms.Timer
.的类。这个类只是封装了
中消息机制的计数器,如果你不准备开发多线程应用,那么就可以用这个类。
在下面的例子中,我们将用
System.Threading.Timer
类,定时器的最简单实现,我们只需要如下定义的构造方法
public Timer(TimerCallback callback,
&& object state,
&& int dueTime,
&& int period);
对于第一个参数(
),我们可以指定定时执行的函数;第二个参数是传递给函数的通用对象;第三个参数是计时器开始执行前的延时;最后一个参数
,是两个执行之间的毫秒数。
下面的例子创建了两个定时器,
class MainApp
&& static void Main()
&&&&& Timer timer1 = new Timer(new TimerCallback(OnTimer), 1, 0, 2000);
&&&&& Timer timer2 = new Timer(new TimerCallback(OnTimer), 2, 0, 3000);
&&&&& Console.ReadLine();
&& static void OnTimer(object obj)
&&&&& Console.WriteLine("Timer: {0} Thread: {1} Is pool thread: {2}",
&&&&&&&&&(int)obj,
&&&&&&&& Thread.CurrentThread.GetHashCode(),
&&& &&&&&Thread.CurrentThread.IsThreadPoolThread);
Timer: 1 Thread: 2 Is pool thread: True
Timer: 2 Thread: 2 Is pool thread: True
Timer: 1 Thread: 2 Is pool thread: True
Timer: 2 Thread: 2 Is pool thread: True
Timer: 1 Thread: 2 Is pool thread: True
Timer: 1 Thread: 2 Is pool thread: True
Timer: 2 Thread: 2 Is pool thread: True
犹如你看到的那样,两个定时器中的所有函数调用都在同一个线程中执行(
),应用程序使用的资源最小化了。
同步对象的执行
相对于定时器,
线程池允许在执行函数上同步对象,为了在多线程环境中的各线程之间共享资源,我们需要用
同步对象。
如果我们没有线程,或者线程必须阻塞直到事件收到信号,就像我前面提到一样,这会增加应用程序中总的线程数量,结果导致系统需要更多的资源和
线程池允许我们把请求进行排队,直到某个特殊的同步对象收到信号后执行。如果这个信号没有收到,请求函数将不需要任何线程,所以可以保证系统性能最优化。
ThreadPool
类提供了下面的方法:
public static RegisteredWaitHandle RegisterWaitForSingleObject(
&& WaitHandle waitObject,
&& WaitOrTimerCallback callBack,
&& object state,
&& int millisecondsTimeOutInterval,
&& bool executeOnlyOnce);
第一个参数
,waitObject
可以是任何继承于
WaitHandle
&&&&&&&& Mutex
&&&& ManualResetEvent
&&&& AutoResetEvent
就像你看到的那样,只有系统的同步对象才能用在这里,就是继承自
WaitHandle
的对象。你不能用其它任何的同步机制,比如
read-write
剩余的参数允许我们指明当一个对象收到信号后执行的函数(
;一个传递给函数的状态
); 线程池等待对象的最大时间
millisecondsTimeOutInterval
) 和一个标识表明对象收到信号时函数只能执行一次,
(executeOnlyOnce
下面的代理声明目的是用在函数的回调:
delegate void WaitOrTimerCallback(
&& object state,
&& bool timedOut);
如果参数 timeout 设置的最大时间已经失效,但是没有同步对象收到信号的花,这个函数就会被调用。
下面的例子用了一个手工事件和一个互斥量来通知线程池中的执行函数:
class MainApp
&& static void Main(string[] args)
&&&&& ManualResetEvent evt = new ManualResetEvent(false);
&&&&& Mutex mtx = new Mutex(true);
&&&&& ThreadPool.RegisterWaitForSingleObject(evt,
&&&&&&&& new WaitOrTimerCallback(PoolFunc),
&&&&&&&& null, Timeout.Infinite, true);
&&&&& ThreadPool.RegisterWaitForSingleObject(mtx,
&&&&&&&& new WaitOrTimerCallback(PoolFunc),
&&&&&&&& null, Timeout.Infinite, true);
&&&&& for(int i=1;i&=5;i++)
&&&&&&&& Console.Write("{0}...", i);
&&&&&&&& Thread.Sleep(1000);
&&&&& Console.WriteLine();
&&&&& evt.Set();
&&&&& mtx.ReleaseMutex();
&&&&& Console.ReadLine();
&& static void PoolFunc(object obj, bool TimedOut)
&&&&& Console.WriteLine("Synchronization object signaled, Thread: {0} Is pool: {1}",
&&&&&&&&&Thread.CurrentThread.GetHashCode(),
&&&&&&&& Thread.CurrentThread.IsThreadPoolThread);
结束显示两个函数都在线程池的同一线程中执行:
1...2...3...4...5...
Synchronization object signaled, Thread: 6 Is pool: True
Synchronization object signaled, Thread: 6 Is pool: True
线程池常见的应用场景就是
操作。多数应用系统需要读磁盘,数据发送到
,因特网连接等等。所有的这些操作都有一些特征,直到他们执行操作时,才需要
框架为所有这些可能执行的异步操作提供了
类。当这些操作执行完后,线程池中特定的函数会执行。尤其是在服务器应用程序中执行多线程异步操作,性能会更好。
在第一个例子中,我们将把一个文件异步写到硬盘中。看一看
FileStream
的构造方法是如何使用的:
public FileStream(
&& string path,
&& FileMode mode,
&& FleAccess access,
&& FleShare share,
&& int bufferSize,
&& bool useAsync);
最后一个参数非常有趣,我们应该对异步执行文件的操作设置
。如果我们没有这样做,即使我们用了异步函数,它们的操作仍然会被主叫线程阻塞。
下面的例子说明了用一旦
FileStream BeginWrite
方法写文件操作结束,线程池中的一个回调函数将会被执行。注意我们可以在任何时候访问
IAsyncResult
接口,它可以用来了解当前操作的状态。我们可以用
CompletedSynchronously
属性指示一个异步操作是否完成,而当一个操作结束时,
IsCompleted
属性会设上一个值。
IAsyncResult
提供了很多有趣的属性,比如:
AsyncWaitHandle
,一旦操作完成,一个异步对象将会被通知。
class MainApp
&& static void Main()
&&&&& const string fileName = "temp.dat";
&&&&& FileS
&&&&& byte[] data = new Byte[10000];
&&&&& IAsyncR
&&&&& fs = new FileStream(fileName,
&&&&&&&&&FileMode.Create,
&&&&&&&&&FileAccess.Write,
&&&&&&&&&FileShare.None,
&&&&&&&&&1,
&&&&&&&&&true);
&&&&& ar = fs.BeginWrite(data, 0, 10000,
&&&&&&&& new AsyncCallback(UserCallback), null);
&&&&& Console.WriteLine("Main thread:{0}",
&&&&&&&& Thread.CurrentThread.GetHashCode());
&&&&& Console.WriteLine("Synchronous operation: {0}",
&&&&&&&& ar.CompletedSynchronously);
&&&&& Console.ReadLine();
&& static void UserCallback(IAsyncResult ar)
&&&&& Console.Write("Operation finished: {0} on thread ID:{1}, is pool: {2}",
&&&&&&&&&ar.IsCompleted,
&&&&&&&&&Thread.CurrentThread.GetHashCode(),
&&&&&&&&&Thread.CurrentThread.IsThreadPoolThread);
输出的结果显示了操作是异步执行的,一旦操作结束后,用户的函数就在线程池中执行。
Main thread:9
Synchronous operation: False
Operation finished: True on thread ID:10, is pool: True
的场景中,由于
操作通常比磁盘操作慢,这时用线程池就显得尤为重要。过程跟前面提到的差不多,
类提供了多个方法用于执行异步操作:
&&&&&&&& BeginRecieve
&&&&&&&& BeginSend
&&&&&&&& BeginConnect
&&&&&&&& BeginAccept
假如你的服务器应用使用了
来与客户端通讯,一定会用到这些方法。这种方法取代了对每个客户端连接都启用一个线程的做法,所有的操作都在线程池中异步执行。
下面的例子用另外一个支持异步操作的类,
HttpWebRequest
用这个类,我们可以建立一个到
服务器的连接。这个方法叫
BeginGetResponse
但在这个例子中有一个很重要的区别。在上面最后一个示例中,我们没有用到从操作中返回的结果。但是,我们现在需要当一个操作结束时从
服务器返回的响应,为了接收到这个信息,
中所有提供异步操作的类都提供了成对的方法。在
HttpWebRequest
这个类中,这个成对的方法就是:
BeginGetResponse
EndGetResponse
版本,我们可以接收操作的结果。在我们的示例中,
EndGetResponse
服务器接收响应。
虽然可以在任何时间调用
EndGetResponse
方法,但在我们的例子中是在回调函数中做的。仅仅是因为我们想知道已经做了异步请求。如果我们在之前调用
EndGetResponse
这个调用将一直阻塞到操作完成。
在下面的例子中,我们发送一个请求到
Microsoft Web
,然后显示了接收到响应的大小。
class MainApp
&& static void Main()
&&&&& HttpWebR
&&&&& IAsyncR
&&&&& request = (HttpWebRequest)WebRequest.CreateDefault(
&&&&&&&& new Uri("http://www.microsoft.com"));
&&&&& ar = request.BeginGetResponse(new AsyncCallback(PoolFunc), request);
&&&&& Console.WriteLine("Synchronous: {0}", ar.CompletedSynchronously);
&&&&& Console.ReadLine();
&& static void PoolFunc(IAsyncResult ar)
&&&&& HttpWebR
&&&&& HttpWebR
&&&&& Console.WriteLine("Response received on pool: {0}",
&&&&&&&& Thread.CurrentThread.IsThreadPoolThread);
&&&&& request = (HttpWebRequest)ar.AsyncS
&&&&& response = (HttpWebResponse)request.EndGetResponse(ar);
&&&&& Console.WriteLine("&Response size: {0}",
&&&&&&&& response.ContentLength);
下面刚开始结果信息表明,异步操作正在执行:
Synchronous: False
过了一会儿,响应接收到了。下面的结果显示:
Response received on pool: True
&& Response size: 27331
就像你看到的那样,一旦收到响应,线程池的异步函数就会执行。
监视线程池
ThreadPool
类提供了两个方法用来查询线程池的状态。第一个是我们可以从线程池获取当前可用的线程数量:
public static void GetAvailableThreads(
&& out int workerThreads,
&& out int completionPortThreads);
从方法中你可以看到两种不同的线程:
WorkerThreads
工作线程是标准系统池的一部分。它们是被
框架托管的标准线程,多数函数是在这里执行的。显式的用户请求(
QueueUserWorkItem
方法),基于异步对象的方法(
RegisterWaitForSingleObject
)和定时器(
CompletionPortThreads
这种线程常常用来
Windows NT, Windows 2000
Windows XP
提供了一个步执行的对象,叫做
IOCompletionPort
和异步对象关联起来,用少量的资源和有效的方法,我们就可以调用系统线程池的异步
操作。但是在
Windows 95, Windows 98,
Windows Me
有一些局限。比如:
在某些设备上,没有提供
IOCompletionPorts
功能和一些异步操作,如磁盘和邮件槽。在这里你可以看到
框架的最大特色:一次编译,可以在多个系统下运行。根据不同的目标平台,
框架会决定是否使用
IOCompletionPorts API
,用最少的资源达到最好的性能。
这节包含一个使用
类的例子。在这个示例中,我们将异步建立一个连接到本地的
服务器,然后发送一个
请求。通过这个例子,我们可以很容易地鉴别这两种不同的线程。
using System.T
using System.N
using System.Net.S
using System.T
namespace ThreadPoolTest
&& class MainApp
&&&&& static void Main()
&&&&&&&& S
&&&&&&&& IPHostEntry hostE
&&&&&&&& IPAddress ipA
&&&&&&&& IPEndPoint ipEndP
&&&&&&&&&hostEntry = Dns.Resolve(Dns.GetHostName());
&&&&&&&& ipAddress = hostEntry.AddressList[0];
&&&&&&&& ipEndPoint = new IPEndPoint(ipAddress, 80);
&&&&&&&& s = new Socket(ipAddress.AddressFamily,
&&&&&&&&&&& SocketType.Stream, ProtocolType.Tcp);
&&&&&&&& s.BeginConnect(ipEndPoint, new AsyncCallback(ConnectCallback),s);
&&&&&&&&&Console.ReadLine();
&&&&& static void ConnectCallback(IAsyncResult ar)
&&&&&&&& byte[]
&&&&&&&& Socket s = (Socket)ar.AsyncS
&&&&&&&& data = Encoding.ASCII.GetBytes("GET /"n");
&&&&&&&& Console.WriteLine("Connected to localhost:80");
&&&&&&&& ShowAvailableThreads();
&&&&&&&& s.BeginSend(data, 0,data.Length,SocketFlags.None,
&&&&&&&&&&& new AsyncCallback(SendCallback), null);
&&&&& static void SendCallback(IAsyncResult ar)
&&&&&&&& Console.WriteLine("Request sent to localhost:80");
&&&&&&&& ShowAvailableThreads();
&&&&& static void ShowAvailableThreads()
&&&&&&&& int workerThreads, completionPortT
&&&&&&&& ThreadPool.GetAvailableThreads(out workerThreads,
&&&&&&&&&&& out completionPortThreads);
&&&&&&&& Console.WriteLine("WorkerThreads: {0}," +
&&&&&&&&&&&&" CompletionPortThreads: {1}",
&&&&&&&&&&& workerThreads, completionPortThreads);
Microsoft Windows NT, Windows 2000, or Windows XP
下运行这个程序,你将会看到如下结果:
Connected to localhost:80
WorkerThreads: 24, CompletionPortThreads: 25
Request sent to localhost:80
WorkerThreads: 25, CompletionPortThreads: 24
如你所看到地那样,连接用了工作线程,而发送数据用了一个完成端口(
CompletionPort
),接着看下面的顺序:
我们得到一个本地
地址,然后异步连接到那里。
在工作线程上执行异步连接操作,因为在
上,不能用
IOCompletionPorts
来建立连接。
一旦连接建立了,
类调用指明的函数
ConnectCallback
,这个回调函数显示了线程池中可用的线程数量。我们可以看到这些是在工作线程中执行的。
请求进行编码后,我们用
方法从同样的函数
ConnectCallback
中发送一个异步请求。
上的发送和接收操作可以通过
IOCompletionPort
来执行异步操作,所以当请求做完后,回调函数就会在一个
CompletionPort
线程中执行。因为函数本身显示了可用的线程数量,所以我们可以通过这个来查看,对应的完成端口数已经减少了多少。
如果我们在
Windows 95, Windows 98,
Windows Me
平台上运行相同的代码,会出现相同的连接结果,请求将被发送到工作线程,而非完成端口。你应该知道的很重要的一点就是,
类总是会利用最优的可用机制,所以你在开发应用时,可以不用考虑目标平台是什么。
你已经看到在上面的例子中每种类型的线程可用的最大数是
。我们可以用
GetMaxThreads
返回这个值:
public static void GetMaxThreads(
&& out int workerThreads,
&& out int completionPortThreads);
一旦到了最大的数量,就不会创建新线程,所有的请求都将被排队。假如你看过
ThreadPool
类的所有方法,你将发现没有一个允许我们更改最大数的方法。就像我们前面提到的那样,线程池是每个处理过程的唯一共享资源。这就是为什么不可能让应用程序域去更改这个配置的原因。想象一下出现这种情况的后果,如果有第三方组件把线程池中线程的最大数改为
,整个应用都会停止工作,甚至在进程中其它的应用程序域都将受到影响。同样的原因,公共语言运行时的宿主也有可能去更改这个配置。比如:
允许系统管理员更改这个数字。
在你的应用程序使用线程池之前,还有一个东西你应该知道:死锁。在线程池中执行一个实现不好的异步对象可能导致你的整个应用系统中止运行。
设想你的代码中有个方法,它需要通过
连接到一个
服务器上。一个可能的实现就是用
BeginConnect
方法异步打开一个连接,然后用
EndConnect
方法等待连接的建立。代码如下:
class ConnectionSocket
&& public void Connect()
&&&&& IPHostEntry ipHostEntry = Dns.Resolve(Dns.GetHostName());
&&&&& IPEndPoint ipEndPoint = new IPEndPoint(ipHostEntry.AddressList[0],
&&&&&&&& 80);
&&&&& Socket s = new Socket(ipEndPoint.AddressFamily, SocketType.Stream,
&&&&&&&& ProtocolType.Tcp);
&&&&& IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null);
&&&&& s.EndConnect(ar);
多快,多好。调用
BeginConnect
使异步操作在线程池中执行,而
EndConnect
一直阻塞到连接被建立。
如果线程池中的一个执行函数中用了这个类的方法,将会发生什么事情呢?设想线程池的大小只有两个线程,然后用我们的连接类创建了两个异步对象。当这两个函数同时在池中执行时,线程池已经没有用于其它请求的空间了,除非直到某个函数结束。问题是这些函数调用了我们类中的
方法,这个方法在线程池中又发起了一个异步操作。但线程池一直是满的,所以请求就一直等待任何空闲线程的出现。不幸的是,这将永远不会发生,因为使用线程池的函数正等待队列函数的结束。结论就是:我们的应用系统已经阻塞了。
我们以此推断
个线程的线程池的行为。假如
个函数都等待异步对象操作的结束。结果将是一样的,死锁一样会出现。
在下面的代码片断中,我们使用了这个类来说明问题:
class MainApp
&& static void Main()
&&&&& for(int i=0;i&30;i++)
&&&&&&&& ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc));
&&&&& Console.ReadLine();
&& static void PoolFunc(object state)
&&&&& int workerThreads,completionPortT
&&&&& ThreadPool.GetAvailableThreads(out workerThreads,
&&&&&&&& out completionPortThreads);
&&&&& Console.WriteLine("WorkerThreads: {0}, CompletionPortThreads: {1}",
&&&&&&&&&workerThreads, completionPortThreads);
&&&&& Thread.Sleep(15000);
&&&&& ConnectionSocket connection = new ConnectionSocket();
&&&&& connection.Connect();
如果你运行这个例子,你将看到池中的线程是如何把线程的可用数量减少到零的,接着应用中止,死锁出现了。
如果你想在你的应用中避免出现死锁,永远不要阻塞正在等待线程池中的其它函数的线程。这看起来很容易,但记住这个规则意味着有两条:
不要创建这样的类,它的同步方法在等待异步函数。因为这种类可能被线程池中的线程调用。
不要在任何异步函数中使用这样的类,如果它正等待着这个异步函数。
如果你想检测到应用中的死锁情况,那么就当你的系统挂起时,检查线程池中的线程可用数。线程的可用数量已经没有并且
的使用率为
,这是很明显的死锁症状。你应该检查你的代码,以确定哪个在线程中执行的函数正在等待异步操作,然后删除它。
有关安全性
如果你再看看
ThreadPool
类,你会看到有两个方法我们没有用到,
UnsafeQueueUserWorkItem
UnsafeRegisterWaitForSingleObject
为了完全理解这些方法,首先,我们必须回忆
框架中安全策略是怎么运作的。
&&&&&&&& &Windows
安全机制是关注资源。操作系统本身允许对文件,用户,注册表键值和任何其它的系统资源设定权限。这种方法对应用系统的用户认证非常有效,但当出现用户对他使用的系统产生
不信任的情况时,这就会有些局限性。例如这些程序是从
下载的。在这种情况下,一旦用户安装了这个程序,它就可以执行用户权限范围内的任何操作。举个例子,假如用户可以删除他公司内的任何共享文件,任何从
下载的程序也都可以这样做。
&&&&&&&& .NET
提供了应用到程序的安全性策略,而不是用户。这就是说,在用户权限的范围内,我们可以限制任何执行单元(程序集)使用的资源。通过
,我们可以根据条件定义一组程序集,然后为每组设置不同的策略,一个典型的例子就是限制从
下载的程序访问磁盘的权限。
为了让这个功能运转起来,
框架必须维护一个不同程序集之间的调用栈。假设一个应用没有权限访问磁盘,但是它调用了一个对整个系统都可以访问的类库,当第二个程序集执行一个磁盘的操作时,设置到这个程序集的权限允许这样做,但是权限不会被应用到主叫程序集,
不仅要检查当前程序集的权限,而且会检查整个调用栈的权限。这个栈已经被高度优化了,但是它们给两个不同程序集之间的调用增加了额外的负担。
UnsafeQueueUserWorkItem
, UnsafeRegisterWaitForSingleObject
QueueUserWorkItem
, RegisterWaitForSingleObject
类似。由于是非安全版本不会维护它们执行函数之间的调用栈,所以非安全版本运行的更快些。但是回调函数将只在当前程序集的安全策略下执行,它就不能应用权限到整个调用栈中的程序集。
的建议是仅在性能非常重要的、安全已经控制好的极端情况下才用非安全版本。例如,你构建的应用程序不会被其它的程序集调用,或者仅被很明确清楚的程序集使
用,那么你可以用非安全版本。如果你开发的类库会被第三方应用程序中使用,那么你就不应该用这些方法,因为它们可能用你的库获取访问系统资源的权限。
在下面例子中,你可以看到用
UnsafeQueueUserWorkItem
方法的风险。我们将构建两个单独的程序集,在第一个程序集中我们将在线程池中创建一个文件,然后我们将导出一个类以使这个操作可以被其它的程序集执行。
using System.T
using System.IO;
namespace ThreadSecurityTest
&& public class PoolCheck
&&&&& public void CheckIt()
&&&&&&&& ThreadPool.QueueUserWorkItem(new WaitCallback(UserItem), null);
&&&&& private void UserItem(object obj)
&&&&&&&& FileStream fs = new FileStream("test.dat", FileMode.Create);
&&&&&&&& fs.Close();
&&&&&&&& Console.WriteLine("File created");
第二个程序集引用了第一个,并且用了
方法去创建一个文件:
namespace ThreadSecurityTest
&& class MainApp
&&&&& static void Main()
&&&&&&&& PoolCheck pc = new PoolCheck();
&&&&&&&& pc.CheckIt();
&&&&&&&& Console.ReadLine();
编译这两个程序集,然后运行
应用。默认情况下,你的应用被配置为允许执行磁盘操作,所以系统成功生成文件。
&&&& File created
现在,打开
框架的配置。为了简化这个例子,我们仅创建一个代码组关联到
应用。接着展开
运行库安全策略
/ All_Code /
,增加一个叫
ThreadSecurityTest
的组。在向导中,选择
条件并导入
到我们的应用中,设置为
级别,并选择“该策略级别将只具有与此代码组关联的权限集中的权限”选项。
运行应用程序,看看会发生什么情况:
Unhandled Exception: System.Security.SecurityException: Request for the
&&&permission of type System.Security.Permissions.FileIOPermission,
&&&&&&mscorlib, Version=1.0.3300.0, Culture=neutral,
&&&&&&&&&PublicKeyToken=b77a5c failed.
我们的策略开始工作,系统已经不能创建文件了。这是因为
框架为我们维护了一个调用栈才使它成为了可能,虽然创建文件的库有权限去访问系统。
现在把库中的
QueueUserWorkItem
UnsafeQueueUserWorkItem
再次编译程序集,然后运行
程序。现在的结果是:
File created
即使我们的系统没有足够的权限去访问磁盘,但我们已经创建了一个向整个系统公开它的功能的库,却没有维护它的调用栈。记住一个金牌规则:
仅在你的代码不允许让其它的应用系统调用,或者当你想要严格限制访问很明确清楚的程序集,才使用非安全的函数。
在这篇文章中,我们知道了为什么在我们的服务器应用中需要使用线程池来优化资源和
的利用。我们学习了一个线程池是如何实现的,需要考虑多个因素如:
使用的百分比,队列请求或者系统的处理器数量。
&&&&&&&& .NET
提供了丰富的线程池的功能以让我们的应用程序使用,
框架的类紧密地集成在一起。这个线程池是高度优化了的,它只需要最少的
时间和资源,而且总能适应目标平台。
因为与框架集成在一起,所以框架中的大部分类都提供了使用线程池的内在功能,给开发人员提供了集中管理和监视应用中的线程池的功能。鼓励第三方组件使用线程池,这样它们的客户就可以享受
所提供的全部功能。允许执行用户函数,定时器,
操作和同步对象。
假如你在开发服务器应用系统,只要有可能就在你的请求处理系统中使用线程池。或者你开发了一个让服务器程序使用的库,那么尽可能提供系统线程池的异步对象处理。
TA的最新馆藏[转]&[转]&[转]&[转]&[转]&
喜欢该文的人也喜欢

我要回帖

更多关于 线程池的大小设置 的文章

 

随机推荐