線程池和異步線程
文件夾:
- 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 最后在頁面中訪問