本專題概要:
- 引言
- 你知道APM嗎?
- 你想知道如何使用異步編程模型編寫代碼嗎?
- 使用委托也可以實現異步編程,你知道否?
- 小結
一、引言
在前面的C#基礎知識系列中介紹了從C#1.0——C#4.0中一些主要特性,然而.NET4.5更新,除了提供了一些新的類和一些新的模板外,對於C#語言也做了一定的更新,最重要的就是.NET4.5(對應於C#5.0)中提供了async和await兩個關鍵字,這兩個關鍵字是我們實現異步編程更加容易了,其實早在.NET1.0開始微軟就對異步編程做了相應的支持——即異步編程模型(APM),之后在.NET2.0中又提出了基於事件的異步編程模型(EAP),.NET4.0中又提出了基於任務的異步編程模型(TAP)。所以為了幫助大家全面理解.NET類庫對異步編程的支持,這里我把我學習異步編程的一些體會和理解分享出來,希望對大家在學習的過程中有所幫助。
在開始講解APM之前,我想先分享一下VisualStudio版本、C#版本和.NET版本的一個對應關系。之所以在這里分享這個對應關系,是因為在C#基礎知識系列的文章發布之后,有些初學者對.NET版本和C#語言特性之間的對應關系有點不清楚,有時候會弄混淆了。並且通過這個對應關系,也可以幫助大家對C#和.NET類庫有個全面的把控,可以幫助大家理清楚C#和.NET類庫中各個知識點,使他們可以對號入坐。具體他們的之間對應關系見下表:
C# 版本 |
.NET Framework版本 |
發布日期
|
特性
|
C# 1.0 |
.NET Framework 1.0 |
2002.1
|
APM |
C# 1.1 |
.NET Framework 1.1 |
2003.4
|
APM |
C# 2.0 |
.NET Framework 2.0 |
2005.11
|
|
C# 3.0 |
.NET Framework 3.0 .NET Framework 3.5 |
2007.11
|
Lambda表達式
|
C# 4.0 |
.NET Framework 4.0 |
2010.4
|
|
C# 5.0 |
.NET Framework 4.5 |
2012.8
|
異步和等待(async和await) 調用方信息(Caller Information)
|
二、你知道APM嗎?
APM即異步編程模型的簡寫(AsynchronousProgramming Model),大家在寫代碼的時候或者查看.NET的類庫的時候肯定會經常看到和使用以BeginXXX和EndXXX類似的方法,其實你在使用這些方法的時候,你就再使用異步編程模型來編寫程序。異步編寫模型是一種模式,該模式允許用更少的線程去做更多的操作,.NETFramework很多類也實現了該模式,同時我們也可以自定義類來實現該模式,(也就是在自定義的類中實現返回類型為IAsyncResult接口的BeginXXX方法和EndXXX方法),另外委托類型也定義了BeginInvoke和EndInvoke方法,並且我們使用WSDL.exe和SvcUtil.exe工具來生成Web服務的代理類型時,也會生成使用了APM的BeginXxx和EndXxx方法。下面就具體就拿FileStream類的BeginRead和EndRead方法來介紹下下異步編程模型的實現。
BeginXxx方法——開始執行異步操作介紹
當需要讀取文件中的內容時,我們通常會采用FileStream的同步方法Read來讀取,該同步方法的定義為:
// 從文件流中讀取字節塊並將該數據寫入給定的字節數組中
// array代表把讀取的字節塊寫入的緩存區
// offset代表array的字節偏量,將在此處讀取字節
// count 代表最多讀取的字節數
public override int Read(byte[] array, int offset, int count )
該同步方法會堵塞執行的線程,當一個WinForm程序需要實現讀取一個大文件的內容然后把內容顯示在界面時,如果我們調用該方法去讀取文件的內容時,此時Read方法會堵塞UI線程,在讀取文件內容沒有完成之前,用戶不能對窗體進行任何的操作,包括關閉應用程序,此時用戶看到的該窗體會出現無法響應,這樣就給用戶帶來不好一個用戶體驗,從用戶角度來看是用戶體驗不好,此時我們自己解決問題的思路肯定是——能不能讓讀取文件操作在另外一個線程中執行,這樣就不會堵塞UI線程,這時候UI線程繼續做屬於自己的事情,即響應用戶的操作。不錯,微軟也肯定也想到了這個解決方案的,並且在實際操作中也是這么做的,即通過BeginRead方法來實現異步編程,使讀取操作不再堵塞UI線程。BeginRead方法代表異步執行Read操作,並返回實現IAsyncResult接口的對象,該對象存儲着異步操作的信息,下面就看下BeginRead方法的定義,看看與同步Read的方法區別在哪里的.
// 開始異步讀操作
// 前面的3個參數和同步方法代表的意思一樣,這里就不說了,可以看到這里多出了2個參數
// userCallback代表當異步IO操作完成時,你希望由一個線程池線程執行的方法,該方法必須匹配AsyncCallback委托
// stateObject代表你希望轉發給回調方法的一個對象的引用,在回調方法中,可以查詢IAsyncResult接口的AsyncState屬性來訪問該對象
public override IAsyncResult BeginRead(byte[] array, int offset, int numBytes, AsyncCallback userCallback, Object stateObject
)
從上面的代碼中可以看出異步方法和同步方法的區別,如果你在使用該異步方法時,不希望異步操作完成后調用任何代碼,你可以把userCallback參數設置為null。該異步方法子所以不會堵塞UI線程是因為調用該方法后,該方法會立即把控制權返回給調用線程(如果是UI線程來調用該方法時,即返回給UI線程),然而同步卻不是這樣,同步方法是等該操作完成之后返回讀取的內容之后才返回給調用線程,從而導致在操作完成之前調用線程就一直等待狀態。
EndXxx方法——結束異步操作介紹
前面介紹完了BeginXxx方法,我們看到所有BeginXxx方法返回的都是實現了IAsyncResult接口的一個對象,並不是對應的同步方法所要得到的結果的。此時我們需要調用對應的EndXxx方法來結束異步操作,並向該方法傳遞IAsyncResult對象,EndXxx方法的返回類型就是和同步方法一樣的。例如,FileStream的EndRead方法返回一個Int32來代表從文件流中實際讀取的字節數。
對於訪問異步操作的結果,APM提供了四種方式供開發人員選擇:
- 在調用BeginXxx方法的線程上調用EndXxx方法來得到異步操作的結果,但是這種方式會阻塞調用線程,知道操作完成之后調用線程才繼續運行
- 查詢IAsyncResult的AsyncWaitHandle屬性,從而得到WaitHandle,然后再調用它的WaitOne方法來使一個線程阻塞並等待操作完成再調用EndXxx方法來獲得操作的結果。
- 循環查詢IAsyncResult的IsComplete屬性,操作完成后再調用EndXxx方法來獲得操作返回的結果。
- 使用 AsyncCallback委托來指定操作完成時要調用的方法,在操作完成后調用的方法中調用EndXxx操作來獲得異步操作的結果。
在上面的4種方式中,第4種方式是APM的首選方式,因為此時不會阻塞執行BeginXxx方法的線程,然而其他三種都會阻塞調用線程,相當於效果和使用同步方法是一樣,個人感覺根本失去了異步編程的特點,所以其他三種方式可以簡單了解下,在實際異步編程中都是使用委托的方式。
通過上面的介紹,大家應該對異步編程模型有了進一步的了解了吧,要識別某個類是否實現了異步編程模型,只需要看是不是有BeginXxx方法(當然返回類型需要是IAsyncResult)和EndXxx方法。其實異步編程模型這個模式,就是微軟利用委托和線程池幫助我們實現的一個模式(該模式利用一個線程池線程去執行一個操作,在FileStream類BeginRead方 法中就是執行一個讀取文件操作,該線程池線程會立即將控制權返回給調用線程,此時線程池線程在后台進行這個異步操作;異步操作完成之后,通過回調函數來獲 取異步操作返回的結果。此時就是利用委托的機制。所以說異步編程模式時利用委托和線程池線程搞出來的模式,包括后面的基於事件的異步編程和基於任務的異步 編程,還有C#5中的async和await關鍵字,都是利用這委托和線程池搞出來的。他們的本質其實都是一樣的,只是后面提出來的使異步編程更加簡單罷了。)
既然這里講到了FileStream對象,這里就提出一個關於該類值得注意的地方的:
FileStream對象默認情況下是同步打開操作系統句柄,當我們創建一個FileStream對象沒有為其指定FileOptions.Asynchronous參數或者沒有顯示指定useAsync為true時,Windows操作系統會以同步的方法執行所有的文件操作,即使此時你還是可以調用BeginRead方法。但是這樣對於你的應用程序,操作只是表面上是異步執行的,但FileStream類在內部會用另一個線程模擬異步行為。
同樣道理,當創建的FileStream對象指定了FileOptions.Asynchronous參數時,然后我們仍然可以調用Read同步方法,此時在內部,FileStream類會開始一個異步操作,並立即使調用線程進入睡眠狀態,知道操作完成才會喚醒,通過這樣來模擬同步行為。因此在使用FileStream對象時,需要先決定是同步執行還是異步執行。並顯示地指定FileOptions.Asynchronous參數或useAsync參數。
三、你想知道如何使用異步編程模型編寫代碼嗎?
介紹了這么久的異步編程模型,大家肯定很迫不及待地想使用異步編程模型來改寫自己的同步應用程序或者實現一個異步的應用程序。下面就通過一個例子來演示如何使用APM來現異步編程(該程序也實現了一個同步方法,為了讓大家更好地體會同步線程和異步線程的區別,本程序的實現是一個控制台程序,大家也可以很好地一直與WinForm應用程序和WPF程序):
#region use APM to download file asynchronously
private static void DownloadFileAsync(string url)
{
try
{
// Initialize an HttpWebRequest object
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
// Create an instance of the RequestState and assign HttpWebRequest instance to its request field.
RequestState requestState = new RequestState();
requestState.request = myHttpWebRequest;
myHttpWebRequest.BeginGetResponse(new AsyncCallback(ResponseCallback), requestState);
}
catch (Exception e)
{
Console.WriteLine("Error Message is:{0}",e.Message);
}
}
// The following method is called when each asynchronous operation completes.
private static void ResponseCallback(IAsyncResult callbackresult)
{
// Get RequestState object
RequestState myRequestState = (RequestState)callbackresult.AsyncState;
HttpWebRequest myHttpRequest = myRequestState.request;
// End an Asynchronous request to the Internet resource
myRequestState.response = (HttpWebResponse)myHttpRequest.EndGetResponse(callbackresult);
// Get Response Stream from Server
Stream responseStream = myRequestState.response.GetResponseStream();
myRequestState.streamResponse = responseStream;
IAsyncResult asynchronousRead = responseStream.BeginRead(myRequestState.BufferRead, 0, myRequestState.BufferRead.Length, ReadCallBack, myRequestState);
}
// Write bytes to FileStream
private static void ReadCallBack(IAsyncResult asyncResult)
{
try
{
// Get RequestState object
RequestState myRequestState = (RequestState)asyncResult.AsyncState;
// Get Response Stream from Server
Stream responserStream = myRequestState.streamResponse;
int readSize = responserStream.EndRead(asyncResult);
if (readSize > 0)
{
myRequestState.filestream.Write(myRequestState.BufferRead, 0, readSize);
responserStream.BeginRead(myRequestState.BufferRead, 0, myRequestState.BufferRead.Length, ReadCallBack, myRequestState);
}
else
{
Console.WriteLine("\nThe Length of the File is: {0}", myRequestState.filestream.Length);
Console.WriteLine("DownLoad Completely, Download path is: {0}", myRequestState.savepath);
myRequestState.response.Close();
myRequestState.filestream.Close();
}
}
catch (Exception e)
{
Console.WriteLine("Error Message is:{0}", e.Message);
}
}
#endregion
運行結果為(從運行結果也可以看出,在主線程中調用 DownloadFileAsync(downUrl)方法時,DownloadFileAsync(downUrl)方法中的myHttpWebRequest.BeginGetResponse調用被沒有阻塞調用線程(即主線程),而是立即返回到主線程,是主線程后面的代碼可以立即執行)
如果我們調用的是同步方法時,此時會堵塞主線程,直到文件的下載操作被完成之后主線程才繼續執行后面的代碼,下面是下載文件的同步方法:
#region Download File Synchrously
private static void DownLoadFileSync(string url)
{
// Create an instance of the RequestState
RequestState requestState=new RequestState();
try
{
// Initialize an HttpWebRequest object
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
// assign HttpWebRequest instance to its request field.
requestState.request = myHttpWebRequest;
requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse();
requestState.streamResponse = requestState.response.GetResponseStream();
int readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
while (readSize > 0)
{
requestState.filestream.Write(requestState.BufferRead, 0, readSize);
readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
}
Console.WriteLine("\nThe Length of the File is: {0}", requestState.filestream.Length);
Console.WriteLine("DownLoad Completely, Download path is: {0}", requestState.savepath);
}
catch (Exception e)
{
Console.WriteLine("Error Message is:{0}", e.Message);
}
finally
{
requestState.response.Close();
requestState.filestream.Close();
}
}
#endregion
使用同步方法下載文件的運行結果為(大家可以對照兩個方式的結果就可以明顯看出他們的區別了。):
四、使用委托也可以實現異步編程,你知道否?
在前面的介紹中已經提到委托類型也會定義了BeginInvoke方法和EndInvoke方法,所以委托類型也實現了異步編程模型,所以可以使用委托的BeginInvoke和EndInvoke方法來回調同步方法從而實現異步編程。因為調用委托的BeginInvoke方法來執行一個同步方法時,此時會使用線程池線程回調這個同步方法並立即返回到調用線程中,由於耗時操作在另外一個線程上運行,所以執行BeginInvoke方法的主線程就不會被堵塞。但是這里存在的一個問題時,因為同步方法在另外一個線程中執行的,然而我們怎么把同步方法執行的狀態反應到UI界面上來呢?因為在GUI應用程序(包括Windows窗體,WPF和Silverlight)中,創建窗口的線程是唯一能夠對那個窗口進行更新的線程,所以在執行同步方法的線程就不能對窗口中的控件進行操作,也就不能把方法允許的結果反應到窗體上了。這里有兩種解決方案,一種是設置控件的CheckForIllegalCrossThreadCalls屬性為false,設置為false的意思代表允許跨線程調用,(這種方式雖然可以解決該問題,但是不推薦,因為它違背了.NET安全規范);第二種就是使用SynchronizationContext基類,該類記錄着線程的同步上下文對象,我們可以通過在GUI線程中調用SynchronizationContext.Current屬性來獲得GUI線程的同步上下文,然后當線程池線程需要更新窗體時,可以調用保存的SynchronizationContext派生對象的Post方法(Post方法會將回調函數送到GUI線程的隊列中,每個線程都有各自的操作隊列的,線程的執行都是從這個隊列中拿方法去執行),向Post方法傳遞要由GUI線程調用的方法(該方法的定義要匹配SendOrPostCallback委托的簽名),還需要想Post方法傳遞一個要傳給回調方法的參數。
4.1 使用委托實現更好的用戶體驗——不堵塞UI線程
雖然第一種方案是一種不推薦的方案,但是我覺得有些朋友還是不知道怎么實現的,所以在這部分就用具體的代碼來實現下,並且該實現也可以與使用同步上下文對象的方式進行對比,這樣大家就可以更加了解如何使用委托來進行異步編程了。下面就具體看實現代碼吧:
View Code
// 定義用來實現異步編程的委托
private delegate string AsyncMethodCaller(string fileurl);
public Mainform()
{
InitializeComponent();
txbUrl.Text = "http://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe";
// 允許跨線程調用
// 實際開發中不建議這樣做的,違背了.NET 安全規范
CheckForIllegalCrossThreadCalls = false;
}
private void btnDownLoad_Click(object sender, EventArgs e)
{
rtbState.Text = "Download............";
if (txbUrl.Text == string.Empty)
{
MessageBox.Show("Please input valid download file url");
return;
}
AsyncMethodCaller methodCaller = new AsyncMethodCaller(DownLoadFileSync);
methodCaller.BeginInvoke(txbUrl.Text.Trim(), GetResult, null);
}
// 同步下載文件的方法
// 該方法會阻塞主線程,使用戶無法對界面進行操作
// 在文件下載完成之前,用戶甚至都不能關閉運行的程序。
private string DownLoadFileSync(stringurl)
{
// Create an instance of the RequestState
RequestState requestState = new RequestState();
try
{
// Initialize an HttpWebRequest object
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
// assign HttpWebRequest instance to its request field.
requestState.request = myHttpWebRequest;
requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse();
requestState.streamResponse = requestState.response.GetResponseStream();
int readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
while (readSize > 0)
{
requestState.filestream.Write(requestState.BufferRead, 0, readSize);
readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
}
// 執行該方法的線程是線程池線程,該線程不是與創建richTextBox控件的線程不是一個線程
// 如果不把 CheckForIllegalCrossThreadCalls 設置為false,該程序會出現“不能跨線程訪問控件”的異常
return string.Format("The Length of the File is: {0}", requestState.filestream.Length) + string.Format("\nDownLoad Completely, Download path is: {0}", requestState.savepath);
}
catch (Exception e)
{
return string.Format("Exception occurs in DownLoadFileSync method, Error Message is:{0}", e.Message);
}
finally
{
requestState.response.Close();
requestState.filestream.Close();
}
}
// 異步操作完成時執行的方法
private void GetResult(IAsyncResult result)
{
AsyncMethodCaller caller = (AsyncMethodCaller)((AsyncResult)result).AsyncDelegate;
// 調用EndInvoke去等待異步調用完成並且獲得返回值
// 如果異步調用尚未完成,則 EndInvoke 會一直阻止調用線程,直到異步調用完成
stringreturnstring= caller.EndInvoke(result);
//sc.Post(ShowState,resultvalue);
rtbState.Text = returnstring;
}
運行的結果為:
4.2 在線程中訪問另一個線程創建的控件
這部分將使用同步上下文的方式來實現在線程池線程中如何更新GUI線程中窗體,因為在程序代碼部分都有詳細的解釋,這里就直接貼代碼了:
public partial class MainForm : Form
{
// 定義用來實現異步編程的委托
private delegate string AsyncMethodCaller(string fileurl);
// 定義顯示狀態的委托
private delegate void ShowStateDelegate(string value);
private ShowStateDelegate showStateCallback;
SynchronizationContext sc;
public MainForm()
{
InitializeComponent();
txbUrl.Text = "http://download.microsoft.com/download/7/0/3/703455ee-a747-4cc8-bd3e-98a615c3aedb/dotNetFx35setup.exe";
showStateCallback = new ShowStateDelegate(ShowState);
}
private void btnDownLoad_Click(object sender, EventArgs e)
{
rtbState.Text = "Download............";
btnDownLoad.Enabled = false;
if (txbUrl.Text == string .Empty)
{
MessageBox.Show("Please input valid download file url");
return;
}
AsyncMethodCaller methodCaller = new AsyncMethodCaller(DownLoadFileSync);
methodCaller.BeginInvoke(txbUrl.Text.Trim(), GetResult, null);
// 捕捉調用線程的同步上下文派生對象
sc = SynchronizationContext.Current;
}
// 同步下載文件的方法
// 該方法會阻塞主線程,使用戶無法對界面進行操作
// 在文件下載完成之前,用戶甚至都不能關閉運行的程序。
private string DownLoadFileSync(string url)
{
// Create an instance of the RequestState
RequestState requestState = new RequestState();
try
{
// Initialize an HttpWebRequest object
HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
// assign HttpWebRequest instance to its request field.
requestState.request = myHttpWebRequest;
requestState.response = (HttpWebResponse)myHttpWebRequest.GetResponse();
requestState.streamResponse = requestState.response.GetResponseStream();
int readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
while (readSize > 0)
{
requestState.filestream.Write(requestState.BufferRead, 0, readSize);
readSize = requestState.streamResponse.Read(requestState.BufferRead, 0, requestState.BufferRead.Length);
}
// 執行該方法的線程是線程池線程,該線程不是與創建richTextBox控件的線程不是一個線程
// 如果不把 CheckForIllegalCrossThreadCalls 設置為false,該程序會出現“不能跨線程訪問控件”的異常
return string .Format("The Length of the File is: {0}", requestState.filestream.Length) + string .Format("\nDownLoad Completely, Download path is: {0}", requestState.savepath);
}
catch (Exception e)
{
return string .Format("Exception occurs in DownLoadFileSync method, Error Message is:{0}", e.Message);
}
finally
{
requestState.response.Close();
requestState.filestream.Close();
}
}
// 異步操作完成時執行的方法
private void GetResult(IAsyncResult result)
{
AsyncMethodCaller caller = (AsyncMethodCaller)((AsyncResult)result).AsyncDelegate;
// 調用EndInvoke去等待異步調用完成並且獲得返回值
// 如果異步調用尚未完成,則 EndInvoke 會一直阻止調用線程,直到異步調用完成
string returnstring = caller.EndInvoke(result);
// 通過獲得GUI線程的同步上下文的派生對象,
// 然后調用Post方法來使更新GUI操作方法由GUI 線程去執行
sc.Post(ShowState,returnstring);
}
// 顯示結果到richTextBox
// 因為該方法是由GUI線程執行的,所以當然就可以訪問窗體控件了
private void ShowState(object result)
{
rtbState.Text = result.ToString();
btnDownLoad.Enabled = true;
}
}
程序的運行結果和前面使用第一方案的結果是一樣的,這里就不重復貼圖了,上面所有的實現都是部分代碼,你可以在文章的最后下載本專題的所有源碼。
五、小結
到這里本專題關於異步編程模型的介紹就結束了,異步編程模型(APM)雖然是.NET 1.0中提出來的一個模式,相對於現在來說是舊了點,並且微軟現在官方也表明在最新的代碼中不推薦使用該模型來實現異步的應用程序,而是推薦使用基於任務的異步編程模型來實現異步的應用程序,但是我個人認為,正是因為它是.NET1.0中提出的來,並且現在來看確實有些舊了,所以我們才更應該好好研究下它,因為后面提出的EAP和TAP微 軟做了更多的封裝,是我們對異步編程的本質都不清楚的(其實它們的本質都是使用線程池和委托機制的,具體可以查看前面的相關部分),並且系統學習下異步編 程,也可以讓我們對新的異步編程模型的所帶來的好處有更可直觀的認識。在后面的一專題我將帶大家全面認識下基於事件的異步編程模型(EAP)。
本專題所有源碼:http://files.cnblogs.com/zhili/APM.rar
出處:http://www.cnblogs.com/zhili/
提示:本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
如果對文章有任何問題,都可以再評論中留言,我會盡可能的答復您,謝謝你的閱讀