標籤:
在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()
開始介紹非同步執行委託之前,首先看看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
- BeginInvoke()方法用來啟動非同步呼叫,它與委託類型具有相同的參數;該例子中為int32 a和int32 b
- BeginInvoke()方法還有兩個選擇性參數:
- AsyncCallback委託,通過這個參數可以指定非同步呼叫完成時要調用的方法(回呼函數)
- 使用者定義物件,該對象可向回調方法傳遞資訊
- BeginInvoke()方法調用後立即返回,不等待委託執行完成;也就是說調用線程不會阻塞,可以繼續執行
- BeginInvoke()將返回實現IAsyncResult介面的對象,這個對象可用於監視非同步呼叫的進度;在準備擷取方法調用的結果時,可以把它傳給EndInvoke()方法
.method public hidebysig newslot virtual instance int32 EndInvoke ( class [mscorlib]System.IAsyncResult result ) runtime managed {} // end of method NumberAdd::EndInvoke
- EndInvoke方法傳回型別就是委託類型的傳回型別,該例子中為int32
- EndInvoke只有一個實現IAsyncResult介面的參數(BeginInvoke()方法的返回結果),結合這個參數EndInvoke()方法可以擷取非同步呼叫的結果
- 在調用BeginInvoke()方法後,可以隨時調用EndInvoke()方法,如果非同步呼叫尚未完成,則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 的成員:
- AsyncState:擷取使用者定義物件,可以作為回呼函數的參數,從調用線程想回呼函數傳遞資訊
- AsyncWaitHandle:擷取用於等待非同步作業完成的 WaitHandle
- CompletedSynchronously:擷取一個值,該值指示非同步作業是否同步完成
- IsCompleted:擷取一個布爾值,該值指示非同步作業是否已完成
所以,代碼中就可以利用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
通過輪詢的方式來檢測非同步呼叫方法的執行狀態也不是一種很好的實現方式,對於非同步方法呼叫的調用,最好的實現方式就是通過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()方法來完成方法的非同步呼叫。
當我們需要執行耗時的操作,又不希望調用線程被阻塞的時候,就可以考慮使用非同步委託。通過委託非同步執行的例子可以看出,其實非同步委託的底層使用了多線程(直接使用線程池中的線程)。所以,使用非同步委託的地方,我們也可以通過多線程的方式實現。
C#非同步委託