本文大部份內容來自於mikeperetz的Asynchronous Method Invocation及本人的一些我的體會所得,但願對你有所幫助。原英文文獻能夠在codeproject中搜索到。編程
這篇文章將介紹異步調用的實現機制及如何調用異步方法。大多數.NET開發者在通過delegate、Thread、AsynchronousInvocation以後,一般都會對以上概念產生混淆及誤用。實際上,以上概念是.NET2.0版本中對並行編程的核心支持,基於概念上的錯誤認識有可能致使在實際的編程中,沒法利用異步調用的特性優化咱們的程序,例如大數據量加載引發的窗體」假死」。事實上這並非一個困難的問題,該文將以一種逐層深刻、抽絲剝繭的方式逐漸深刻到異步編程的學習中。異步
大多數人並不喜歡閱讀大量的文字說明,而喜歡直接閱讀代碼,所以,咱們在下文中將主要以代碼的形式闡述同步與異步的調用。異步編程
假設咱們有一個函數,它的功能是將當前線程掛起3秒鐘。函數
staticvoid Sleep() { Thread.Sleep(3000); }
一般,當你的程序在調用Sleep後,它將等待3秒鐘的時間,在這3秒鐘時間內,你不能作任何其餘操做。3秒以後,控制權被交回給調用線程(一般也就是你的主線程,即WinForm程序的UI線程)。這種類型的調用稱爲同步,本次調用順序以下:性能
● 調用Sleep();學習
● Sleep()執行中;大數據
● Sleep()執行完畢,控制權歸還調用線程。優化
咱們再次調用Sleep()函數,不一樣的是,咱們要基於委託來完成此次調用。通常爲了將函數綁定在委託中,咱們要定義與函數返回類型、參數值徹底一致的委託,這稍有點麻煩。但.NET內部已經爲咱們定義好了一些委託,例如MethodInvoker,這是一種無返回值、無參數的委託簽名,這至關於你自定義了一種委託:spa
public delegate void SimpleHandler();
執行如下代碼:線程
MethodInvoker invoker =new MethodInvoker(Sleep); invoker.Invoke();
咱們使用了委託,但依然是同步的方式。主線程仍然要等待3秒的掛起,而後獲得響應。
注意:Delegate.Invoke是同步方式的。
如何在調用Sleep()方法的同時,使主線程能夠沒必要等待Sleep()的完成,一直可以獲得相應呢?這很重要,它意味着在函數執行的同時,主線程依然是非阻塞狀態。在後臺服務類型的程序中,非阻塞的狀態意味着該應用服務能夠在等待一項任務的同時去接受另外一項任務;在傳統的WinForm程序中,意味着主線程(即UI線程)依然能夠對用戶的操做獲得響應,避免了」假死」。咱們繼續調用Sleep()函數,但此次要引入BeginInvoke。
MethodInvoker invoker =new MethodInvoker(Sleep); invoker.BeginInvoke(null, null);
● 注意BeginInvoke這行代碼,它會執行委託所調用的函數體。同時,調用BeginInvoke方法的線程(如下簡稱爲調用線程)會當即獲得響應,而沒必要等待Sleep()函數 的完成。
● 以上代碼是異步的,調用線程徹底能夠在調用函數的同時處理其餘工做,可是不足的是咱們仍然不知道對於Sleep()函數的調用什麼時候會結束,這是下文將要解決的問 題。
● BeginInvoke能夠以異步的方式徹底取代Invoke,咱們也沒必要擔憂函數包含參數的狀況,下文介紹傳值問題。
注意:Delegate.BeginInvoke是異步方式的。若是你要執行一項任務,但並不關心它什麼時候完成,咱們就可使用BeginInvoke,它不會帶來調用線程的阻塞。
一旦你使用.NET完成了一次異步調用,它都須要一個線程來處理異步工做內容(如下簡稱異步線程),異步線程不多是當前的調用線程,由於那樣仍然會形成調用線程的阻塞,與同步無異。事實上,.NET會將全部的異步請求隊列加入線程池,以線程池內的線程處理全部的異步請求。對於線程池彷佛沒必要了解的過於深刻,但咱們仍須要關注如下幾點內容:
● Sleep()的異步調用會在一個單獨的線程內執行,這個線程來自於.NET線程池。
● .NET線程池默認包含25個線程,你能夠改變這個值的上限,每次異步調用都會使用其中某個線程執行,但咱們並不能控制具體使用哪個線程。
● 線程池具有最大線程數目上限,一旦全部的線程都處於忙碌狀態,那麼新的異步調用將會被置於等待隊列,直到線程池產生了新的可用線程,所以對於大量異步請 求,咱們有必要關注請求數量,不然可能形成性能上的影響。
爲了暴露線程池的上限,咱們修改Sleep()函數,將線程掛起的時間延長至30s。在代碼的運行輸出結果中,咱們須要關注如下內容:
● 線程池內的可用線程數量。
● 異步線程是否來自於線程池。
● 線程託管ID值。
上文已經提到,.NET線程池默認包含25個線程,所以咱們連續調用30次異步方法,這樣能夠在第25次調用後,看看線程池內部究竟發生了什麼。
privatevoid Sleep() { int intAvailableThreads, intAvailableIoAsynThreds; // 取得線程池內的可用線程數目,咱們只關心第一個參數便可 ThreadPool.GetAvailableThreads(out intAvailableThreads, out intAvailableIoAsynThreds); // 線程信息 string strMessage = String.Format("是不是線程池線程:{0},線程託管ID:{1},可用線程數:{2}", Thread.CurrentThread.IsThreadPoolThread.ToString(), Thread.CurrentThread.GetHashCode(), intAvailableThreads); Console.WriteLine(strMessage); Thread.Sleep(30000); } privatevoid CallAsyncSleep30Times() { // 建立包含Sleep函數的委託對象 MethodInvoker invoker =new MethodInvoker(Sleep); for (int i =0; i <30; i++) { // 以異步的形式,調用Sleep函數30次 invoker.BeginInvoke(null, null); } }
對於輸出結果,咱們能夠總結爲如下內容:
● 全部的異步線程都來自於.NET線程池。
● 每次執行一次異步調用,便產生一個新的線程;同時可用線程數目減小。
● 在執行異步調用25次後,線程池中再也不有空閒線程。此時,應用程序會等待空閒線程的產生。
● 一旦線程池內產生了空閒線程,它會當即被分配給異步任務等待隊列,以後線程池中仍然不具有空閒線程,應用程序主線程進入掛起狀態繼續等待空閒線程,這樣 的調用一直持續到異步調用被執行完30次。
針對以上結果,咱們對於異步調用能夠總結爲如下內容:
● 每次異步調用都在新的線程中執行,這個線程來自於.NET線程池。
● 線程池有本身的執行上限,若是你想要執行屢次耗費時間較長的異步調用,那麼線程池有可能進入一種」線程飢餓」狀態,去等待可用線程的產生。
咱們已經知道,如何在不阻塞調用線程的狀況下執行一個異步調用,但咱們沒法得知異步調用的執行結果,及它什麼時候執行完畢。爲了解決以上問題,咱們可使用EndInvoke。EndInvoke在異步方法執行完成前,都會形成線程的阻塞。所以,在調用BeginInvoke以後調用EndInvoke,效果幾乎徹底等同於以阻塞模式執行你的函數(EndInvoke會使調用線程掛起,一直到異步函數執行完畢)。可是,.NET是如何將BeginInvoke和EndInvoke進行綁定呢?答案就是IAsyncResult。每次咱們使用BeginInvoke,返回值都是IAsyncResult類型,它是.NET追蹤異步調用的關鍵值。每次異步調用以後的結果如何?若是要了解具體執行結果,IAsyncResult即可視爲一個標籤。經過這個標籤,你能夠了解異步調用什麼時候執行完畢,更重要的是,它能夠保存異步調用的參數傳值,解決異步函數上下文問題。
咱們如今經過幾個例子來了解IAsyncResult。若是以前對它瞭解很少,那麼就須要耐心的將它領悟,由於這種類型的調用是.NET異步調用的關鍵內容。
class Program { static private void SleepOneSecond(int UserID) { // 當前線程掛起1秒 Thread.Sleep(1000); Console.WriteLine("你輸入的UserID爲:{0}",UserID ); } public delegate void MethodInvoker(int UserID); public static void Main(string[] args) { // 建立一個指向SleepOneSecond的委託 MethodInvoker invoker =new MethodInvoker(SleepOneSecond); Console.Write("請輸入UserID"); int UserID = Convert.ToInt32(Console.ReadLine()); // 開始執行SleepOneSecond,但此次異步調用咱們傳遞一些參數 // 觀察Delegate.BeginInvoke()的第二個參數 for (int i = 0; i < 3; i++) { IAsyncResult tag = invoker.BeginInvoke(UserID , null, "passing some " + i + " state"); // 應用程序在此處會形成阻塞,直到SleepOneSecond執行完成 invoker.EndInvoke(tag); // EndInvoke執行完畢,取得以前傳遞的參數內容 string strState = (string)tag.AsyncState; Console.WriteLine("EndInvoke的傳遞參數" + strState); } Console.ReadKey(); } } 請輸入UserID 1903 你輸入的UserID爲:1903 EndInvoke的傳遞參數passing some 0 state 你輸入的UserID爲:1903 EndInvoke的傳遞參數passing some 1 state 你輸入的UserID爲:1903 EndInvoke的傳遞參數passing some 2 state
回到文章初始提到的」窗體動態更新」問題,若是你將上述代碼運行在一個WinForm程序中,會發現窗體依然陷入」假死」。對於這種狀況,你可能會陷入疑惑:以前說異步函數都執行在線程池中,所以能夠確定異步函數的執行不會引發UI線程的忙碌,但爲何窗體依然陷入了」假死」?問題就在於EndInvoke。EndInvoke此時扮演的角色就是」線程鎖」,它充當了一個調用線程與異步線程之間的調度器,有時調用線程須要使用異步函數的執行結果,那麼調度線程就須要在異步執行完以前一直等待,直到獲得結果方可繼續運行。EndInvoke一方面負責監聽異步函數的執行情況,一方面將調用線程掛起。
所以在Win Form環境下,UI線程的」假死」並非由於線程忙碌形成,而是被EndInvoke」善意的」暫時封鎖,它只是爲了等待異步函數的完成。
咱們能夠對EndInvoke總結以下:
● 在執行EndInvoke時,調用線程會進入掛起狀態,一直到異步函數執行完成。
● 使用EndInvoke可使應用程序得知異步函數什麼時候執行完畢。
● 若是將上述寫法稱爲」異步」,你必定以爲這種」異步」徒具其名,雖然知道異步函數什麼時候執行完畢,也獲得了異步函數的傳值,但咱們的調用線程仍然會等待函數執行完畢,在等待過程當中線程阻塞,實際上與同步調用無異。
如今咱們把問題稍微複雜化,考慮異步函數拋出異常的一種情形。咱們須要瞭解在何處捕捉到異常,是BeginInvoke,仍是EndInvoke?甚至是有沒有可能沒法捕捉異常?答案是EndInvoke。BeginInvoke的工做只是開始線程池對於異步函數的執行工做,EndInvoke則須要處理函數執行完成的全部信息,包括其中產生的異常。
class Program { public delegate void MethodInvoker(); static private void SleepOneSecond() { // 當前線程掛起1秒 Thread.Sleep(1000); throw new Exception("Here Is An Async Function Exception"); } public static void Main(string[] args) { // 建立一個指向SleepOneSecond的委託 MethodInvoker invoker =new MethodInvoker(SleepOneSecond); // 開始執行SleepOneSecond,但此次異步調用咱們傳遞一些參數 // 觀察Delegate.BeginInvoke()的第二個參數 IAsyncResult tag = invoker.BeginInvoke(null, "passing some state"); try { // 應用程序在此處會形成阻塞,直到SleepOneSecond執行完成 invoker.EndInvoke(tag); } catch (System.Exception ex) { Console.WriteLine(ex.Message); } // EndInvoke執行完畢,取得以前傳遞的參數內容 string strState = (string)tag.AsyncState; Console.WriteLine("EndInvoke的傳遞參數" + strState); Console.ReadKey(); } } Here Is An Async Function Exception EndInvoke的傳遞參數passing some state
執行以上代碼後,你將發現只有在使用EndInvoke時,纔會捕捉到異常,不然異常將丟失。須要注意的是,直接在編譯器中運行程序是沒法產生捕獲異常的,只有在Debug、Release環境下運行,異常纔會以對話框的形式直接彈出。
如今咱們來改變一下異步函數,讓它接收一些參數。
class Program { public delegate string DelegateWithParameters(int param1, string param2, ArrayList param3); static private string FuncWithParameters(int param1, string param2, ArrayList param3) { // 咱們在這裏改變參數值 param1 = 200; param2 = "hello"; param3 = new ArrayList(); return "thank you for reading me"; } public static void Main(string[] args) { // 建立幾個參數 string strParam = "Param1"; int intValue = 100; ArrayList list = new ArrayList(); list.Add("Item1"); // 建立委託對象 DelegateWithParameters delFoo = new DelegateWithParameters(FuncWithParameters); // 調用異步函數 IAsyncResult tag = delFoo.BeginInvoke(intValue, strParam, list, null, null); // 一般調用線程會當即獲得響應 // 所以你能夠在這裏進行一些其餘處理 // 執行EndInvoke來取得返回值 string strResult = delFoo.EndInvoke(tag); Console.WriteLine("param1: " + intValue); Console.WriteLine("param2: " + strParam); Console.WriteLine("ArrayList count: " + list.Count); Console.WriteLine("返回值: " + strResult); Console.ReadKey(); } } //param1: 100 //param2: Param1 //ArrayList count: 1 //返回值: thank you for reading me
咱們的異步函數對參數的改變並無影響其傳出值,如今咱們把ArrayList變爲ref參數,看看會給EndInvoke帶來什麼變化。
class Program { public delegate string DelegateWithParameters(out int param1, string param2, ref ArrayList param3); static private string FuncWithParameters(out int param1, string param2, ref ArrayList param3) { // 咱們在這裏改變參數值 param1 = 200; param2 = "hello"; param3 = new ArrayList(); return "thank you for reading me"; } public static void Main(string[] args) { // 建立幾個參數 string strParam = "Param1"; int intValue = 100; ArrayList list = new ArrayList(); list.Add("Item1"); // 建立委託對象 DelegateWithParameters delFoo = new DelegateWithParameters(FuncWithParameters); // 調用異步函數 IAsyncResult tag = delFoo.BeginInvoke(out intValue, strParam, ref list, null, null); // 一般調用線程會當即獲得響應 // 所以你能夠在這裏進行一些其餘處理 // 執行EndInvoke來取得返回值 string strResult = delFoo.EndInvoke(out intValue,ref list,tag); Console.WriteLine("param1: " + intValue); Console.WriteLine("param2: " + strParam); Console.WriteLine("ArrayList count: " + list.Count); Console.WriteLine("返回值: " + strResult); Console.ReadKey(); } } //param1: 200 //param2: Param1 //ArrayList count: 0 //返回值: thank you for reading me
param2沒有變化,由於它是輸入參數;param1做爲輸出參數,被更新爲300;ArrayList的值已被從新分配,咱們能夠發現它的引用被指向了一個空元素的ArrayList對象(初始引用已丟失)。經過以上實例,咱們應該能理解參數是如何在BeginInvoke與EndInvoke之間傳遞的。如今咱們來嘗試完成一個非阻塞模式下的異步調用,這是個重頭戲!