在C#中,委托類型是一個類型安全的、面向對象的函數指針。當我們通過delegate關鍵字定義一個委托類型後,編譯器會給委托類型生成三個方法:Invoke、BeginInvoke和EndInvoke。
例如對於下面委托類型,可以通過ILSpy查看編譯器生成的三個方法。
private delegate int NumberAdd(int a, int b);
在使用委托的應用中,最常見的就是通過Invoke()方法以同步方式執行委托實例。也就是說,調用委托的線程將會一直等待,直到委托調用完成。
下面看一個同步執行委托的例子,在numberAdd委托實例中,通過Sleep(3000)模擬了一個耗時的操作:
NumberAdd numberAdd = (a, b) => { Thread.Sleep(3000); Console.WriteLine("----> NumberAdd() on thread is {0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine("----> start to calc {0} + {1}",a,b); Console.WriteLine("----> test result is {0}",a+b); return a + b; }; Console.WriteLine("main thread (id: {0}) invoke numberAdd function", Thread.CurrentThread.ManagedThreadId); //int result = numberAdd(2, 5); int result = numberAdd.Invoke(2, 5); Console.WriteLine("main thread (id: {0}) get the result: {1}", Thread.CurrentThread.ManagedThreadId, result);
代碼的輸出為下,從結果中可以看出,主線程執行委托實例過程中將會被阻塞,直到委托實例執行完成,主線程才會繼續執行。
在很多應用中,一個方法可能要執行很久,例如加載一個很大的文檔,或者執行一個耗時的數據庫操作。如果我們使用同步的方式執行方法,那麼主線程會一直阻塞,直到這個方法執行完成,表現就是應用程序沒有相應,影響用戶體驗。這時,就可以考慮通過委托的異步性進行方法調用。
開始介紹異步執行委托之前,首先看看BeginInvoke()和EndInvoke()方法,結合上面的委托類型NumberAdd來分析一下這兩個方法的參數和返回類型。
.method public hidebysig newslot virtual instance class [mscorlib]System.IAsyncResult BeginInvoke ( int32 a, int32 b, class [mscorlib]System.AsyncCallback callback, object 'object' ) runtime managed { } // end of method NumberAdd::BeginInvoke
.method public hidebysig newslot virtual instance int32 EndInvoke ( class [mscorlib]System.IAsyncResult result ) runtime managed { } // end of method NumberAdd::EndInvoke
介紹過BeginInvoke()和EndInvoke()方法後,看一個異步調用方法的例子。
Console.WriteLine("main thread (id: {0}) invoke numberAdd function", Thread.CurrentThread.ManagedThreadId); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, null, null); Console.WriteLine("main thread (id: {0}) can do something after BeginInvoke", Thread.CurrentThread.ManagedThreadId); for (int i = 0; i < 5; i++) Console.WriteLine("......"); Console.WriteLine("main thread (id: {0}) wait at EndInvoke", Thread.CurrentThread.ManagedThreadId);
//主線程將在EndInvoke調用處阻塞,知道異步調用執行完成為止 int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result);
代碼的輸出為,可以看到其實異步委托使用了一個新的線程來執行numberAdd實例,這樣主線程就不會在BeginInvoke後阻塞,可以繼續執行;但是當主線程執行到EndInvoke時,由於異步調用還沒有完成,主線程將在EndInvoke處阻塞。
其實這個例子中還是有很大的問題,主線程還是會被阻塞。下面進行一點點改進,通過輪詢的方式查看異步調用狀態。
在IAsyncResult接口中,通過實現這個接口的實例的IsCompleted屬性,可以檢測異步操作是否已完成的指示,如果操作完成則為True,否則為False
簡單看看IAsyncResult 的成員:
所以,代碼中就可以利用IsCompleted來獲取異步操作的狀態:
Console.WriteLine("main invoke numberAdd function"); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, null, null); while (iAsyncResult.IsCompleted != true) { Console.WriteLine("main thread can do something after BeginInvoke"); Console.WriteLine("......"); Thread.Sleep(500); } int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result);
同樣,IAsyncResult類型實例中還有一個AsyncWaitHandle屬性���通過這個屬性可以實現更加靈活的等待邏輯。
該屬性返回一個WaitHandle類型的實例,通過這個實例的WaitOne方法就可以調用線程和異步方法之間的同步:
Console.WriteLine("main invoke numberAdd function"); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, null, null); while (!iAsyncResult.AsyncWaitHandle.WaitOne(500)) { Console.WriteLine("main thread can do something after BeginInvoke"); Console.WriteLine("......"); } int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result);
通過輪詢的方式來檢測異步調用方法的執行狀態也不是一種很好的實現方式,對於異步方法的調用,最好的實現方式就是通過AsyncCallback委托來指定回調函數。這樣在異步方法完成後,異步線程將會主動通知調用線程。
下面例子中提供了一個回調函數NumberAddCompleted,當異步方法執行完成後,回調函數就會被調用。
注意:回調函數中需要得到委托實例,然後才可以調用EndInvoke方法來獲取異步方法執行的結果,所以例子中使用了“System.Runtime.Remoting.Messaging”命名空間中的AsyncResult類型來獲取委托的實例。
Console.WriteLine("main invoke numberAdd function"); IAsyncResult iAsyncResult = numberAdd.BeginInvoke(2, 5, NumberAddCompleted, "this is a msg from main"); for (int i = 0; i < 5; i++) { Console.WriteLine("main thread can do something after BeginInvoke"); Console.WriteLine("......"); Thread.Sleep(500); } private static void NumberAddCompleted(IAsyncResult iAsyncResult) { Console.WriteLine("numberAdd function complete, run callback function"); AsyncResult asyncResult = (AsyncResult)iAsyncResult; Console.WriteLine(asyncResult.AsyncState as string); //通過AsyncResult類型實例獲取委托實例 NumberAdd numberAdd = (NumberAdd)asyncResult.AsyncDelegate; int result = numberAdd.EndInvoke(iAsyncResult); Console.WriteLine("main thread get the result: {0}", result); }
本文介紹了C#編譯器為委托類型生成的BeginInvoke()和EndInvoke()方法。通過了一下簡單的例子演示了如何通過BeginInvoke()和EndInvoke()方法來完成方法的異步調用。
當我們需要執行耗時的操作,又不希望調用線程被阻塞的時候,就可以考慮使用異步委托。通過委托異步執行的例子可以看出,其實異步委托的底層使用了多線程(直接使用線程池中的線程)。所以,使用異步委托的地方,我們也可以通過多線程的方式實現。