线程池和异步线程
文件夹:
- 1 什么是CLR线程池?
- 2 简介下线程池各个长处的实现细节
- 3 线程池ThreadPool的经常用法介绍
- 4 简单理解下异步线程
- 5 异步线程的工作过程和几个重要的元素
- 6 有必要简介下Classic Async Pattern 和Event-based Async Pattern
- 7 异步线程的发展趋势以及.net4.5异步的简化
- 8 本章演示样例
- 自己定义一个简单的线程池
- Asp.net异步IHttpAsyncHandler演示样例
- 9 本章总结
在上一章中通过Thread对象创建我们所须要的线程,可是创建线程的开销是非常大的。在须要以性能为重的项目中这的确easy导致一些性能问题,
事实上我们所想象中的线程开销最好例如以下表示:
1 尽量少的创建线程而且能将线程重复利用 2 最好不要销毁而是挂起线程达到避免性能损失 3 通过一个技术达到让应用程序一个个运行工作,类似于一个队列 4 假设某一线程长时间挂起而不工作的话,须要彻底销毁而且释放资源 5 假设线程不够用的话可以创建线程,而且用户可以自己定制最大线程创建的数量 |
令人欣慰的是微软早就想到了以上几点。于是CLR线程池的概念出现了,说究竟线程池就是一个帮助我们开发者实现多线程的一个方案,就是
用来存放“线程”的对象池,利用线程池我们能够开发出性能比較高的对于多线程的应用,同一时候减低一些不必要的性能损耗。我们不必去手动创建
线程,线程池依据给定线程池中的任务队列的队列速度和相关任务运行速度相比較去自己加入或复用线程。关于线程池的细节我会在下文中具体阐述
让我们依据上节中线程池已经实现了5个长处来具体介绍下线程池的功能
1 尽量少的创建线程而且能将线程重复利用
初始化的线程池中是没有线程的。当应用程序区请求线程池时,线程池会制造一个初始线程。普通情况下,线程池会反复使用这个线程来经量少的创
建线程,这样线程池就能尽量避免去创建新的线程而降低的创建线程的开销
2 最好不要销毁而是挂起线程达到避免性能损失
当一个线程池中的线程工作完成之后。该线程不会被销毁而是被挂起操作等待。关于线程的挂起大家能够參考第一篇。假设应用程序重新请求线程
池的话,那么这个线程会又一次被唤醒,从而是实现了线程的复用而且避免一定的性能损失
3 通过一个技术达到让应用程序一个个运行工作,类似于一个队列
多个应用程序请求线程池后,线程池会将各个应用程序排队处理,首先利用线程池中的一个线程对各个应用程序进行操作,假设应用程序的运行速度
超过了队列的排队速度时。线程池会去创建一个新的线程。否则复用原来的线程
4 假设某一线程长时间挂起而不工作的话,须要彻底销毁而且释放资源
有可能在多个程序请求线程池运行后。线程池中产生了很多挂起的线程,而且这些线程池中的线程会一直处于空暇状态间接导致的内存的浪费,所以微软
为线程池设定了一个超时时间。当挂起的线程超时之后会自己主动销毁这些线程
5 假设线程不够用的话可以创建线程
前面已经提到过,有时候排在队列中的当中一个或多个应用程序工作时间超过了规定的每一个应用程序的排队时间,那么线程池不会坐视无论,线程池会创建
一个新的线程来帮助还有一个须要运行的应用程序
相信大家看完上述5个长处及其细节后,对线程池的目的和长处就豁然开朗了
个人觉得CLR线程池最牛的地方就是它可以依据队列中的应用程序运行时间和各个排队应用程序间的 排队速度进行比較,从而决定是不是创建或者复用原先的线程,假如一系列的应用程序很的简单 或者运行速度非常快的情况下,根本无需创建新的线程,从而这个单一线程能够悠闲的挂起等待排队 的下一个应用程序。 假设应用程序很复杂或者层次不齐。那么正好相反。因为这个线程正在忙。 所以无暇对排队的下个任务进行处理,所以须要创建一个新的线程处理,这样陆陆续续会创建一些 新的线程来完毕队列中的应用程序。假设在运行过程中多余线程会超时自己主动回收,并且CLR线程 池同意用户自己定义加入最大线程数和最小线程数,可是出于性能的考虑微软不建议开发者手动更 改线程池中的线程数量。对于以上几点大家务必理解 |
假设您理解了线程池目的及长处后,让我们温故下线程池的经常使用的几个方法:
1. public static Boolean QueueUserWorkItem(WaitCallback wc, Object state);
WaitCallback回调函数就是前文所阐述的应用程序,通过将一些回调函数放入线程池中让其形成队列,然后线程池会自己主动创建或者复用线程
去运行处理这些回调函数,
State: 这个參数也是很重要的,当运行带有參数的回调函数时。该參数会将引用传入,回调方法中。供其使用
3. public static bool SetMaxThreads(int workerThreads,int completionPortThreads);
4. public static bool SetMinThreads(int workerThreads,int completionPortThreads);
3和4方法 CLR线程池类中预留的两个可以更改,线程池中的工作线程和I/O线程数量的方法。
使用该方法时有两点必须注意:
1.不能将辅助线程的数目或 I/O 完毕线程的数目设置为小于计算机的处理器数目。
2.微软不建议程序猿使用这两个方法的原因是可能会影响到线程池中的性能
我们通过一个简单的样例来温故下
using System; using System.Threading; namespace ThreadPoolApplication { class Program { //设定任务数量 static int count = 5; static void Main(string[] args) { //关于ManualResetEvent大伙不必深究,兴许章将会具体阐述,这里因为如果 //让线程池运行5个任务所以也为每一个任务加上这个对象保持同步 ManualResetEvent[] events=new ManualResetEvent[count]; Console.WriteLine("当前主线程id:{0}",Thread.CurrentThread.ManagedThreadId); //循环每一个任务 for (int i = 0; i < count; i++) { //实例化同步工具 events[i]=new ManualResetEvent(false); //Test在这里就是任务类。将同步工具的引用传入能保证共享区内每次仅仅有一个线程进入 Test tst = new Test(events[i]); Thread.Sleep(1000); //将任务放入线程池中,让线程池中的线程运行该任务 ThreadPool.QueueUserWorkItem(tst.DisplayNumber, new { num1=2}); } //注意这里,设定WaitAll是为了堵塞调用线程(主线程)。让其余线程先运行完成, //当中每一个任务完成后调用其set()方法(收到信号),当全部 //的任务都收到信号后。运行完成,将控制权再次交回调用线程(这里的主线程) ManualResetEvent.WaitAll(events); Console.ReadKey(); } } public class Test { ManualResetEvent manualEvent; public Test(ManualResetEvent manualEvent) { this.manualEvent = manualEvent; } public void DisplayNumber(object a) { Console.WriteLine("当前运算结果:{0}",((dynamic)a).num1); Console.WriteLine("当前子线程id:{0} 的状态:{1}", Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.ThreadState); //这里是方法运行时间的模拟,如果凝视该行代码,就能看出线程池的功能了 //Thread.Sleep(30000); //这里是释放共享锁,让其它线程进入 manualEvent.Set(); } } }
运行结果:
从显示结果可以看出线程池仅仅创建了id为9,10,11这3个线程来处理这5个任务。由于每一个任务的运行时间很短,所以线程池
的优势被展现出来了
假设我们去掉DisplayNumber方法中的Thread.Sleep(30000) 的凝视的话,会发现因为任务的运行时间远远超于任务在队列中的
排队时间。所以线程池开启了5个线程来运行任务
在非常多时候比如UI或者IO操作时我们希望将这些非常复杂且耗时比較长的逻辑交给后台线程去处理,而不想影响页面的正常执行,并且
我们希望后台线程可以触发一个回调事件来提示该任务已经完毕,所以基于这样的需求越来越多并且在复杂的逻辑下也难以避免一些多线
程的死锁,所以微软为我们提供了一个属于微软自己的异步线程的概念,上一章提到了多线程和异步的基本概念和差别大家能够去温故下。
线程异步指的是一个调用请求发送给被调用者,而调用者不用等待其结果的返回,一般异步运行的任务都须要比較长的时间, |
相信大家理解的异步的概念后都能对异步的根源有个初步的认识,和线程一样。异步也是针对运行方法而设计的。也就是说当我们运行一个
方法时,使用异步方式能够不阻碍主线程的执行而独立执行,直到执行完成后触发回调事件,注意,.net异步线程也是通过内部线程池建立
的。尽管微软将其封装了起来。可是我们也必须了解下
因为托付是方法的抽象,那么假设托付上能设定异步调用的话,方法也能实现异步,所以本节用异步托付来解释下异步线程的工作过程
前文和前一章节中提到了多线程和异步的差别,对于异步线程来说,这正是体现了其工作方式:
调用者发送一个请求 -> 调用者去做自己的事情 -> 请求会异步运行 -> 运行完成能够利用回调函数告诉调用者(也能够不用) |
在具体说明这几个过程之前,让我们来了解下以下的几个重要的元素
AsyncCallback 托付
事实上这个托付是微软给我们提供的用于异步运行方法体后通知该异步方法已经完毕。AsyncCallBack抽象了全部异步方法运行后回调函数(方法)
,它规定了回调函数(方法)必须拥有一个IAsyncResult的參数而且没有返回值,
IAsyncResult 接口
让我们先来看下msdn上关于它的解释
- IAsyncResult 接口由包括可异步操作的方法的类实现。
它是启动异步操作的方法的返回类型,也是结束异步操作的方法的第三个參数的类型
- 当异步操作完毕时,IAsyncResult 对象也将传递给由 AsyncCallback 托付调用的方法
对于第一条的解释。下面两条代码可以直观的理解:
有时候主线程须要等待异步运行后才干运行,尽管这违背的异步的初衷可是还是能够纳入可能的需求行列,所以假设我们在beginInoke 后立马使用EndInvoke的话。主线程(调用者)会被堵塞,直到异步线程运行完成后在启动运行 |
对于第二条的解释:
结束异步操作时须要使用的回调方法,这里IAsyncResult作为參数被传递进了个这方法。这时IAsyncResult起到了向回调方
法传递信息的作用,关于这点会在后文的异步线程的工作过程中详解下
我们最后再来看下IAsyncResult的几个重要属性
在这里再次强调下IAsyncResult第一个属性AsyncState的作用,就像前面所说,有时我们须要将回调函数的參数传入到回调方法体中,
当然传入入口在BeginInvoke的第二个參数中。在回调函数体中我们能够通过将这个属性类型转换成和BeginInvoke第二个參数一摸
一样的类型后加以使用
关于IAsyncResult最后另一点补充:
假设IAsyncResult本身的功能还不能满足你的须要的话,能够自己定义实现自己的AsyncResult类,但必须实现这个接口 |
理解了以上两个关于异步至关重要的2个元素后。让我们进入一段段代码,在来具体看下异步线程的运行过程
//定义一个托付 public delegate void DoSomething(); static void Main(string[] args) { //1.实例化一个托付,调用者发送一个请求,请求运行该方法体(还未运行) DoSomething doSomething = new DoSomething( () => { Console.WriteLine("假设托付使用beginInvoke的话,这里便是异步方法体"); //4。实现完这种方法体后自己主动触发以下的回调函数方法体 }); //3 。调用者(主线程)去触发异步调用,採用异步的方式请求上面的方法体 IAsyncResult result= doSomething.BeginInvoke( //2.自己定义上面方法体运行后的回调函数 new AsyncCallback ( //5.以下是回调函数方法体 //asyncResult.AsyncState事实上就是AsyncCallback托付中的第二个參数 asyncResult => { doSomething.EndInvoke(asyncResult); Console.WriteLine(asyncResult.AsyncState.ToString()); } ) , "BeginInvoke方法的第二个參数就是传入AsyncCallback中的AsyncResult.AsyncState,我们使用时能够强转成相关类型加以使用"); //DoSomething......调用者(主线程)会去做自己的事情 Console.ReadKey(); }
大家细致看这面这段很easy的代码,为了大家理解方便我特意为异步运行过程加上了特有的凝视和序列号,这种话,大伙能直观初步的理解了异步的运行过程。
让我们依据序列号来说明下:
1. 实例化一个托付。调用者发送一个请求,请求运行该方法体(还未运行) 首先将委实例化而且定义好托付所请求的方法体,可是这个时候方法体是不会执行的 2. 这时候和第一步所相似的是,这里能够将定义好的回调函数AsyncCallback 方法体写入BeginInvoke的第一个參数,将须要传入回调方法体的參数放入第二个參数 3.调用者(主线程)去触发异步调用(运行BeginInvoke方法)。採用异步的方式运行托付中的方法体 4.实现完这种方法体后自己主动触发以下的AsyncCallback中的方法体回调函数(能够设定回调函数为空来表示不须要回调) 5 . 运行回调函数方法体。注意使用托付的 EndInvoke方法结束异步操作,而且输出显示传入异步回调函数的參数 再次强调第五点: (1) 因为使用了回调函数。所以必定异步方法体已经运行过了,所以在回调函数中使用EndInvoke方法是不会堵塞的, (2) 能通过EndInvoke方法获得一些返回结果,比如FileStream.EndRead()可以返回读取的字节数等等 |
6 有必要简介下Classic Async Pattern 和Event-based Async Pattern
首先介绍下Classic Async Pattern:
事实上Classic Async Pattern指的就是我们常见的BeginXXX和EndXXX
IAsyncResult 异步设计模式通过名为 BeginOperationName 和 EndOperationName 的两个方法来实现原同步方法的异步调用
让我们再来回想下.net中的几个的BeginXXX 和EndXXX
Stream中的BeginRead,EndRead,BeginWrite,EndWrite Socket中的BeginReceive。EndReceive HttpWebRequest的BeginGetRequestStream和EndGetRequestStream.... |
再来介绍下Event-based Async Pattern
Event-based Async Pattern 值的是类似于 xxxxxxxAsync() 和 类似于event xxxxxCompleteHander
通过一个方法和一个完毕事件来处理异步操作
.net中的样例:
WebClient.DownloadStringAsync(string uri)和 event DownloadStringCompleteEventHandler |
事实上Classic Async Pattern和Event-based Async Pattern都是一种异步的设计思路。我们也能够依据这一系列的
思路去实现自己的异步方法
微软貌似如今把精力放在win8或WinPhone的metro上,并且记得在win 8开发人员培训的会议上,着重阐述了微软对于异步的支持将越来越强,并且为了快
速响应诸如移动设备的应用程序,微软也在争取为每一个方法都实现一个异步版本号…..可见异步的重要性,相信异步的发展趋势是个不错的
上升曲线,还没反应过来.net4.5的异步新特性便诞生了。首先经历过异步摧残的我们,都会有这样一个感受,往往回调方法和普通方法
会搞错。在复杂的项目面前,有时候简直无法维护,到处都是回调函数。眼花缭乱 所以微软为了简化异步的实现过程,甚至大刀阔斧将
回调函数做成看起来像同步方法,尽管认为非常诡异。还是让我们初步了解下这样的异步的新特性
先看代码
/// <summary> /// .net 4.5 中 async 和 await 全新的keyword 一起实现异步的简化 /// </summary> void async ShowUriContent(string uri) { using (FileStream fs = File.OpenRead("你的文件地址")) { using (FileStream fs2 = new FileStream()) { byte[] buffer = new byte[4096]; //FileStream的ReadAsync方法也是net4.5版本号出现的。它返回一个Task<int>对象 //并且作用于await后的异步代码会等待堵塞直到异步方法完毕后返回 int fileBytesLength = await fs.ReadAsync(buffer,0,buffer.Length).ConfigureAwait(false); while(fileBytesLength>0) { //FileStream的WriteAsync方法也是net4.5版本号出现的 await fs2.WriteAsync(buffer,0,buffer.Length).ConfigureAwait(false); } } } }
相信看完代码后大家有耳目一新的感觉,不错,原本异步调用的回调函数不见了,取而代之的是await和方法声明上的asynckeyword,新特性同意
我们实现这俩个keyword后便能在方法中实现“同步方式”的异步方法。事实上这攻克了一些棘手的问题。诸如原本须要在回调事件里才干释放的文件句
柄在这里和同步方法一样,使用using便搞定了,还有截获异常等等,都不用像之前那样痛苦了,这里另一些东东须要关注下,大家先不用去深
究ConfigureAwait这种方法,因为ReadAsync和 WriteAsync方法是.net 4.5新加的属于返回Task<int>类型的方法所以使用ConfigureAwait
方法可以将数值取到,关于Task泛型类我会在今后的章节中具体阐述
自己定义一个简单的线程池
static void Main(string[] args) { ThreadStart[] startArray = { new ThreadStart(()=>{ Console.WriteLine("第一个任务"); }), new ThreadStart(()=>{Console.WriteLine("第二个任务");}), new ThreadStart(()=>{Console.WriteLine("第三个任务");}), new ThreadStart(()=>{Console.WriteLine("第四个任务");}), }; MyThreadPool.SetMaxWorkThreadCount(2); MyThreadPool.MyQueueUserWorkItem(startArray); Console.ReadKey(); } /// <summary> /// 自己定义一个简单的线程池,该线程池实现了默认开启线程数 /// 当最大线程数所有在繁忙时,循环等待。仅仅到至少一个线程空暇为止 /// 本演示样例使用BackgroundWorker模拟后台线程。任务将自己主动进入队列和离开 /// 队列 /// </summary> sealed class MyThreadPool { //线程锁对象 private static object lockObj = new object(); //任务队列 private static Queue<ThreadStart> threadStartQueue = new Queue<ThreadStart>(); //记录当前工作的任务集合,从中能够推断当前工作线程使用数,假设使用int推断的话可能会有问题, //用集合的话还能取得对象的引用,比較好 private static HashSet<ThreadStart> threadsWorker = new HashSet<ThreadStart>(); //当前同意最大工作线程数 private static int maxThreadWorkerCount = 1; //当前同意最小工作线程数 private static int minThreadWorkerCount = 0; /// <summary> /// 设定最大工作线程数 /// </summary> /// <param name="maxThreadCount">数量</param> public static void SetMaxWorkThreadCount(int maxThreadCount) { maxThreadWorkerCount =minThreadWorkerCount>maxThreadCount?minThreadWorkerCount : maxThreadCount; } /// <summary> /// 设定最小工作线程数 /// </summary> /// <param name="maxThreadCount">数量</param> public static void SetMinWorkThreadCount(int minThreadCount) { minThreadWorkerCount = minThreadCount > maxThreadWorkerCount ?
maxThreadWorkerCount : minThreadCount; } /// <summary> /// 启动线程池工作 /// </summary> /// <param name="threadStartArray">任务数组</param> public static void MyQueueUserWorkItem(ThreadStart[] threadStartArray) { //将任务集合都放入到线程池中 AddAllThreadsToPool(threadStartArray); //线程池运行任务 ExcuteTask(); } /// <summary> /// 将单一任务增加队列中 /// </summary> /// <param name="ts">单一任务对象</param> private static void AddThreadToQueue(ThreadStart ts) { lock (lockObj) { threadStartQueue.Enqueue(ts); } } /// <summary> /// 将多个任务增加到线程池的任务队列中 /// </summary> /// <param name="threadStartArray">多个任务</param> private static void AddAllThreadsToPool(ThreadStart[] threadStartArray) { foreach (var threadStart in threadStartArray) AddThreadToQueue(threadStart); } /// <summary> /// 运行任务,推断队列中的任务数量是否大于0,假设是则推断当前正在使用的工作线程的 /// 数量是否大于等于同意的最大工作线程数,假设一旦有线程空暇的话 /// 就会运行ExcuteTaskInQueen方法处理任务 /// </summary> private static void ExcuteTask() { while (threadStartQueue.Count > 0) { if (threadsWorker.Count < maxThreadWorkerCount) { ExcuteTaskInQueen(); } } } /// <summary> /// 运行出对列的任务,加锁保护 /// </summary> private static void ExcuteTaskInQueen() { lock (lockObj) { ExcuteTaskByThread( threadStartQueue.Dequeue()); } } /// <summary> /// 实现细节。这里使用BackGroudWork来实现后台线程 /// 注冊doWork和Completed事件,当运行一个任务前,前将任务增加到 /// 工作任务集合(表示工作线程少了一个空暇)。一旦RunWorkerCompleted事件被触发则将任务从工作 /// 任务集合中移除(表示工作线程也空暇了一个) /// </summary> /// <param name="threadStart"></param> private static void ExcuteTaskByThread(ThreadStart threadStart) { threadsWorker.Add(threadStart); BackgroundWorker worker = new BackgroundWorker(); worker.DoWork += (o, e) => { threadStart.Invoke(); }; worker.RunWorkerCompleted += (o, e) => { threadsWorker.Remove(threadStart); }; worker.RunWorkerAsync(); } }
显示结果:
Asp.net异步IHttpAsyncHandler演示样例
有时我们须要使用IHttpAsyncHandler来异步实现一些特定的功能,让我用非常easy的演示样例来阐述这个过程
1:首先编写Handler1的逻辑,注意要继承IHttpAsyncHandler接口
/// <summary> /// 异步IHttpHandler,实现了一个简单的统计流量的功能, /// 因为是演示样例代码。所以没有推断IP或者MAC /// </summary> public class Handler1 : IHttpAsyncHandler { //默认訪问量是0 static int visitCount = 0; /// <summary> /// 这个HttpHandler的同步方法 /// </summary> /// <param name="context"></param> public void ProcessRequest(HttpContext context) { } public bool IsReusable { get { return false; } } /// <summary> /// 实现IHttpAsyncHandler 接口方法 /// </summary> /// <param name="context">当前HttpContext</param> /// <param name="cb">回调函数</param> /// <param name="extraData"></param> /// <returns></returns> public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { //这里能够加上推断IP或mac的方法 visitCount++; //实例化AsyncUserVisiteCounterResult对象 AsyncUserVisiteCounterResult result = new AsyncUserVisiteCounterResult(cb, visitCount, context); result.Display(); return result; } /// <summary> /// 结束本次IHttpAsyncHandler工作时触发的request方法 /// </summary> /// <param name="result"></param> public void EndProcessRequest(IAsyncResult result) { } } /// <summary> /// 自己定义IAsyncResult 实现我们额外的Display方法 /// </summary> public class AsyncUserVisiteCounterResult : IAsyncResult { //回调參数 private object _param; //是否异步运行完毕 private bool _asyncIsComplete; //回调方法 private AsyncCallback _callBack; //当前上下文 private HttpContext _context; public AsyncUserVisiteCounterResult(AsyncCallback callBack, object stateParam, HttpContext context) { this._callBack = callBack; this._param = stateParam; _asyncIsComplete = false; this._context = context; } public object AsyncState { get { return this._param; } } /// <summary> /// 等待句柄用于同步功能,关于等待句柄会在兴许章节陈述 /// </summary> public System.Threading.WaitHandle AsyncWaitHandle { get { return null; } } /// <summary> /// 该属性表示不须要异步任务同步完毕 /// </summary> public bool CompletedSynchronously { get { return false; } } /// <summary> /// 该属性表示异步任务是否已经运行完毕 /// </summary> public bool IsCompleted { get { return this._asyncIsComplete; } } /// <summary> /// 自己定义的额外功能,须要注意的是。运行完异步功能后 /// 要将_asyncIsComplete设置为true表示任务运行完毕并且 /// 运行回调方法。否则异步工作无法结束页面会卡死 /// </summary> public void Display() { //这里先不用waitHandle句柄来实现同步 lock (this) { this._context.Response.Write("你是第" + (int)this._param + "位訪问者,訪问时间:"+DateTime.Now.ToString()); this._asyncIsComplete = true; this._callBack(this); } } }
2 在web.config中加入对应的配置,注意path指的是.ashx所在的路径,指的是对应的文件类型
<httpHandlers>
<add verb="*" path="AsyncThreadInAsp.net.Handler1.ashx" type="AsyncThreadInAsp.net.Handler1"/>
</httpHandlers>
3 最后在页面中訪问