.NetFramework的異步編程模型從本質上來說是使用線程池來完成異步的任務,異步委托、HttpWebRequest等都使用了異步模型。
這裡我們使用異步委托來說明異步編程模型。
首先,我們來明確一下,對於多線程來說,我們需要關注哪些問題。
“線程是一段執行中的代碼流”,從這句話中,我們可以關注這段代碼流何時開始執行、何時結束、從主線程如何傳遞參數至從子線程、從子線程如何返回結果至主線程?
問題1:在異步編程模型中,子線程何時開始執行?
對於異步編程模型來說,使用BeginXXX來開始執行線程。
我們首先來看一個實例(來自《C#高級編程(第7版)》)。
pulic delegate int TestMethodDelegate(int data,int ms);
static int TestMethod(int data,int ms)
{
Console.WriteLine("TestMethod started.");
Thread.Sleep(ms);
Console.WriteLine("TestMethod completed.");
return data++;
}
static Main()
{
TestMethodDelegate dl=TestMethod;
IAsyncResult ar=dl.BeginInvoke(1,3000,null,null);
//其中,1,3000分別對應委托的兩個參數,後面兩個null位置的參數是兩個固定的參數,是留給異步回調的時候用的(稍後會講到)。
}
問題2:在異步編程模型中,子線程何時結束?
其實,這個問題對於只有一個主線程的單線程程序來說不會成為問題,因為一段代碼執行過程中,執行到這個方法的最後一行,這個方法也就執行結束了。
但在多線程中,由於主線程與子線程有一個從屬關系,加上線程還有前台線程與後台線程之分,使得子線程的結束時機需要加以判斷,判斷規則如下。
(1).如果子線程是後台線程,那麼主線程結束時,不管子線程是否執行完畢,子線程將結束。
(2).如果子線程是前強線程,那麼主線程結束時,子線程將繼續執行,直至結束。
PS:由於線程池裡面的線程均為後台線程,因此,異步編程模型中,只會存在第1種情況。
那麼問題來了:如果子線程為後台線程,且子線程的處理時間非常地長,那主線程如果處理到最後一條代碼時,那子線程就會關閉,那子線程的處理邏輯就沒走完,就會出錯。
我們該如何防止這樣的問題發生呢?
直觀地來說,在主線程中等待子線程完成是一種可行的方案,而要實現這種方案,我們至少需要知道子線程是否已經執行完成?
在異步編程模型中,我們可以使用輪詢(Polling)、等待句柄(WaitHandler)、EndInvoke()方法來達到這樣的效果。
方法1:輪詢(Polling)
我們改寫Main()方法如下:
static Main()
{
TestMethodDelegate dl=TestMethod;
IAsyncResult ar=dl.BeginInvoke(1,3000,null,null);
while(!ar.IsCompleted)
{
//do something
}
//執行到這裡的時候,子線程已經執行完畢了。
}
方法2:等待句柄(WaitHandler)
我們再次改寫Main()方法。
static Main()
{
TestMethodDelegate dl=TestMethod;
IAsyncResult ar=dl.BeginInvoke(1,3000,null,null);
while(true)
{
if(ar.AsyncWaitHandler.WaitOne(50,false))//每等待50毫秒,然後判斷是否子線程是否已完成,如果已完成,則break。
{
//執行到這裡的時候,子線程已經執行完畢了。
break;
}
}
//主線程....
}
方法3:EndInvoke()方法
再次改用Main()方法:
static Main()
{
TestMethodDelegate dl=TestMethod;
IAsyncResult ar=dl.BeginInvoke(1,3000,null,null);
ar.EndInvoke();//會一直等待,直到委托方法執行完成,其實,也可以通過它將子線程的結果返回至主線程中,可以參考後面的問題4.
//主線程....
}
問題3:在異步編程模型中,從主線程如何傳遞參數至從子線程?
這個問題我們其實已經在問題1中有所展示,我們在BeginInvoke的方法中向委托中傳遞了兩個參數。
問題4:在異步編程模型中,從子線程如何返回結果至主線程?
在問題2中,我們使用EndInvoke()等待委托方法執行完成,其實我們也可采用這個方法得到子線程的返回值。
static Main()
{
TestMethodDelegate dl=TestMethod;
IAsyncResult ar=dl.BeginInvoke(1,3000,null,null);
int result=ar.EndInvoke();//得到返回值,返回值類型這裡為int,即委托定義的返回值類型。
//主線程....
}
問題5:異步回調
異步回調很有用,它的出現應該說是極大地擴展了異步模型下的多線程威力。為什麼這麼說?
考慮一種情況,如果主線程需要120秒完成執行,子線程需要80秒,在主線程第90秒的時候開始有啟動子線程,主線程等待子線程結束,最後結束主線程。那麼,整個主線程就至少需要90+80秒的時間來完成執行,中間很明顯有不少主線程等待子線程的時間。有沒有更好的辦法?
如果主線程不需要子線程的返回結果,而子線程結束後還會有一些單獨的處理邏輯(比如清理對象、單獨的處理數據等),我們可以采用異步回調來解決這個問題。
異步回調:簡而言之,就是異步任務完成之後,再回過頭來調用的方法。
先看一個異步回調的例子:
static Main()
{
TestMethodDelegate dl=TestMethod;
IAsyncResult ar=dl.BeginInvoke(1,3000,TestMethodCompleted,dl);
//主線程....
}
static TestMethodCompleted(IAsyncResult ar)
{
//do something
TestMethodDelegate dl=(TestMethodDelegate )ar.AysncState;
dl.EndInvoke();//可以傳遞委托實例進來,在這裡獲得委托線程的結果。
}
在這裡,我們關注BeginInvoke方法的第3個和第4個參數。
第3個參數,TestMethodCompleted即是回調方法,它是一個AsyncCallback類型的委托。
第4個參數是傳遞給回調方法的參數,在回調方法中可以用ar.AsyncState來獲得這個參數。
使用回調方法,必須注意這個方法要從委托線程中調用,而不是從主線程中調用,如果從主線程中調用,那就變成了普通的方法調用。
另外,如果使用Lambda表達式可以實現一些更簡潔優雅的偌。
static Main()
{
TestMethodDelegate dl=TestMethod;
dl.BeginInvoke(1,3000,
ar=>{
int result=dl.EndInvoke(ar);
//do something
},
null) ;
//主線程....
}
//這裡,不需要把一個值賦予BeginInvoke()方法的最後一個參數,因為Lambda表達式可以直接訪問該作用域外部的變量dl。但是,Lambsa表達式的實現代碼仍是從委托線程中調用,只是不是很明顯。