异步操作(一)
笔者想说说这里为什么谈异步操作。其实这里跟笔者大学学到的系统结构相似,笔者还记得老师要笔者经常做的习题,就是许多外部设备并行工作,具体流程是这样的:设备1向CPU发出数据传送请求,CPU处理请求,下达命令,并发生中断,设备1通过通道或DMA方式进行管理数据的传送,然后是设备2向CPU发出数据请求,CPU处理请求,下达命令,......设备1向CPU发出数据传送完毕请求,CPU作出相应的处理(主要是完成数据传送完毕的收尾处理,还原到断点等等相关操作),也有可能是设备2数据传送完毕向CPU发出数据传送完毕请求,CPU作出相应的处理。这里体现了一个异步操作。如果这里用到的是同步操作的话,过程是这样的设备1向CPU发出数据传送请求,CPU处理请求,并等待数据传送完毕(CPU空闲),数据传送完毕CPU作出相应的处理。设备2.....。这里相信大家能明白一些用异步操作的好处了。所以我们在.NET中编程也应该考虑下这方面的情况,适当用些异步编程,提高程序的效率。
线程概述
线程是程序最终执行者,它不拥有资源,但是有自己的堆栈和一些寄存器。多线程的提供增强了系统健壮性。如果某个进程的线程由于某种原因进入了无限循环或者被挂起后,这时一个线程被冻结,而其他进程的线程和系统本身还可以继续运行。
其实线程的系统开销很大。创建一个线程的开销:系统必须为线程分配并初始化一个线程内核对象,还必须为每个线程保留1Mb的地址空间用于线程的用户模式堆栈,分配12KB的地址空间用于线程的内核模式堆栈。Windows调用进程中每个dll都有一个函数来通知进程中所有的dll操作系统创建一个新的线程。同样销毁一个线程的开销也不小:进程中的每个dll都要接受一个关于该线程即将“死亡”的通知,并且堆栈要释放。相信大家学过操作系统。多个线程如何运行这里就不在介绍,但是必须说明一点多个线程运行,对系统的开销很大。
总之,应该尽可能限制线程的使用。如果创建的线程越多,给操作系统带来的开销就越大,而且所有应用程序运行得越慢。也会占用更多的内存。
CLR线程池简介
CLR中包含管理线程池(thread pool)的代码。它就是为了改进上述的现象。我们可以将线程池看做应用程序自己使用的线程集合。每个进程都有一个线程池,这个线程池被该进程中的所有应用程序域共享。
当CLR初始化时,线程池中还没有任何线程。从内部实现上讲,线程池维护了一系列操作请求。应用程序希望执行一个异步操作时,可以调用一些方法在线程池的队列中加入一个条目。线程池中的代码将从这个队列中提取出条目,并将该条目分配到线程池中的线程。如果线程中没有任何线程,就创建一个新线程。创建一个线程会有相关性能损失。但是,当线程池中的线程完成任务时,并不会被销毁,而是返回到线程池中,在线程池中空闲,等待另外的请求。因为线程不对它自身进行销毁,所以此处不会带来性能损失。
如果应用程序对线程池进行了很多的请求,那么线程池将试图用一个线程来响应所有的请求。但是如果应用程序排队的请求超出了线程池的处理能力,线程池中将创建另外的线程,最终应用程序排队的请求与线程池中的线程的处理能力达到一个平衡点,我们用较少的线程来处理所有请求。
如果应用程序停止请求线程池,如果线程池中的线程空闲时间超过大约2分钟,线程将会唤醒自己,并终止自己,以释放内存资源,并会存在一个性能损失。但是不会太严重,因为线程终止自己的时候,线程应经处于空闲状态,也就是说应用程序当前没有执行太多的事情。
因此我们可以看出线程池它管理创建较少的线程以避免浪费资源与创建较多的线程以利用多处理器,超线程处理器和多核处理器之间的平衡。线程池也是启发式地,如果应用程序需要执行许多任务,而且CPU的数量足够使用,那么线程池将创建更多的线程。如果应用程序的工作负载降下来,那么线程池中的线程将终止自己。
线程池将线程池中的线程进行分类,划分为工作进程(worker thread)和I/O线程(I/O thread).当应用程序请求线程池执行一个受计算限制的异步操作时使用工作线程。而I/O线程用于在受I/O限制的异步操作完成时通知代码。
CLR的线程池允许开发人员设置工作线程和I/O线程的最大数量。CRL保证创建的线程数量不会超过这个设置值。在CLR2.0中将工作线程默然最大数量设置为25,I/O线程最大限制为1000个。
执行异步操作
为将一个受计算限制的异步操作加入到线程池的队列中,一般可以调用ThreadPool类中定义的方法:
static Boolean QueueUserWorkItem(WaitCallback callBack);
static Boolean QueueUserWorkItem(WaitCallback callback,Object state);
static Boolean UnsafeQueueUserWorkItem(WaitCallback callback,Ojbect State)
上述方法将一个“工作项”加入到线程池的队列中,然后这些方法就会立即返回。工作项是一个由CallBack参数标识的方法,线程池的线程将调用该方法。该方法可以只传递一个单独的由state参数指定的参数。没有state参数的QuereUserWrokItem方法为回调函数传递null。最终线程池中的一些线程将执行工作项,从而导致我们的方法被调用。我们这里写的回调方法必须匹配System.Threadding.WaitCallBack委托类型,定义如下:
delegate void WaitCallBack(Ojbect state);
同时给出一个异步调用例子:
using System;
using System.Threading;
public static class Progrom
{
public static void Main()
{
Console.WriteLine(“Main thread:queuing an asynchronous operation”);
//委托采用了简写
ThreadPool.QueueUserWorkItem(CoputeBoundOp,5);
Console.WriteLine(“Main thread:Doing other work here”);
//模拟其他工作
Thread.Sleep(10000);
}
//该方法由线程池中的线程执行
private static void ComputeBoundOp(Object state)
{
Console.Write(“In ComputeBoundOp:State={0}”,state);
Thread.Sleep(1000);
//这个方法返回后,线程就回到线程池中,然后等待执行另一个任务
}
}
UnsafeQueueUserWorkItem方法与QueueUserWorkItem非常相似。它们区别:试图访问一个受限的资源(例如,打开一个文件)时,CLR将执行一个代码访问安全检查。即,CLR将检查调用的线程的调用堆栈中的所有程序集时候都有访问资源的许可权限。如果有一些程序集没有所需的许可权限,CLR将抛出一个SecurityExcepation异常。假设正在执行代码的线程所在的程序集没有打开文件的权限,那么在线程试图打开文件时,CLR将抛出一个SecurityException异常。为了让线程继续运行,线程可以在线程池的队列中加入一个工作项,让线程池的线程来执行打开文件的代码。当然这个必须在拥有合适许可权限的程序集中进行。这种“工作区”智取安全权限的现象可以允许恶意的代码对受限资源进行严重破坏。为了阻止这中获得安全权限的方式,QueueUserWorkItem方法内部遍历调用线程堆栈,并捕获所有被授予的安全权限。然后,当线程池中的线程开始执行时,这些权限在于线程结合。因此,线程池中的线程以调用QueueUserWorkItem方法的线程相同的权限集来完成运行。遍历线程堆栈并捕获所有安全权限与性能紧密相关。如果希望改进受计算限制的异步操作的排队性能,可以调用UnsafeQueueUserWorkItem方法,该方法只将工作项加入线程池的队列中,而不是遍历所有的线程堆栈。但是仅当确认线程池中的线程执行的代码不触及受限的资源时,或者确信接触这部分资源不会出现问题时,才调用该方法。调用UnsafeQueueUserWorkItem方法的代码需要使SecuriityPermission的ControlPolicy标记和ControlEvidence标记开启,可以阻止未信任的代码偶然或者故意提升它的许可权限。