天天看點

一起談.NET技術,.NET中的異步程式設計(二)- 傳統的異步程式設計

  對于很多人來說,異步就是使用背景線程運作耗時的操作。在有些時候這是對的,而在我們日常大部分場景中卻不對。

  比如現在我們有這麼一個需求:使用HttpWebRequest請求某個指定URI的内容,然後輸出在界面上的文本域中。同步代碼很容易編寫:

private void btnDownload_Click(object sender,EventArgs e)

{

var request = HttpWebRequest.Create("http://www.sina.com.cn");

var response = request.GetResponse();

var stream = response.GetResponseStream();

using(StreamReader reader = new StreamReader(stream))

var content = reader.ReadToEnd();

this.txtContent.Text = content;

}

  哦,這個時候你想起了異步。回憶上篇文章的示意圖。我們發現隻要我們将耗時的操作放到另外一個線程上執行就可以了,這樣我們的UI線程可以繼續響應使用者的操作。

  如是你寫下了下面的代碼:

var downloadThread = new Thread(Download);

downloadThread.Start();

private void Download()

  哦,你寫完上面的代碼後發現UI不再阻塞了。心裡想,異步也不過如此嘛。過了一會兒你突然想起,你好像在哪本書裡看到過說盡量不要自己聲明Thread,而應用使用線程池。如是你搜尋了一下MSDN,将上面的代碼改成下面這個樣子:

ThreadPool.QueueUserWorkItem((state) => {Download();});

  嗯,很容易完成了。你都有點佩服自己了,這麼短的時間居然連線程池這麼“進階的技術”都給使用上了。就在你沾沾自喜的時候,你的一個同僚走過來說:你這種實作方式是非常低效的,這裡要進行的耗時操作屬于IO操作,不是計算密集型,可以不配置設定線程給它(雖然不算準确,但如果不深究的話就這麼認為吧)。

  你的同僚說的是對的。對于IO操作(比如讀寫磁盤,網絡傳輸,資料庫查詢等),我們是不需要占用一個thread來執行的。現代的磁盤等裝置,都可以與CPU同時工作,在磁盤尋道讀取這段時間CPU可以幹其他的事情,當讀取完畢之後通過中斷再讓CPU參與進來。是以上面的代碼,雖然建構了響應靈敏的界面,但是卻建立了一個什麼也不幹的線程(當進行網絡請求這段時間内,該線程會被一直阻塞)。是以,如果你要進行異步時首先要考慮,耗時的操作屬于計算密集型還是IO密集型,不同的操作需要采用不同的政策。對于計算密集型的操作你是可以采用上面的方法的:比如你要進行很複雜的方程的求解。是采用專門的線程還是使用線程池,也要看你的操作的關鍵程度。

  這個時候你又在思考,不讓我使用線程,又要讓我實作異步。這該怎麼辦呢?微軟早就幫你想到了這點,在.NET Framework中,幾乎所有進行IO操作的方法幾乎都提供了同步版本和異步版本,而且微軟為了簡化異步的使用難度還定義了兩種異步程式設計模式:

  這種方式就是提供兩個方法實作異步程式設計:比如System.IO.Stream的Read方法:

public int Read(byte[] buffer,int offset,int count);

  它還提供了兩個方法實作異步讀取:

public IAsyncResult BeginRead(byte[] buffer, int offset,int count,AsyncCallback callback);

public int EndRead(IAsyncResult asyncResult);

  以Begin開頭的方法發起異步操作,Begin開頭的方法裡還會接收一個AsyncCallback類型的回調,該方法會在異步操作完成後執行。然後我們可以通過調用EndRead獲得異步操作的結果。關于這種模式更詳細的細節我不在這裡多闡述,感興趣的同學可以閱讀《CLR via C#》26、27章,以及《.NET設計規範》裡對異步模式的描述。在這裡我會使用這種模式重新實作上面的代碼片段:

private static readonly int BUFFER_LENGTH = 1024;

request.BeginGetResponse((ar) => {

var response = request.EndRequest(ar);

ReadHelper(stream,0);

},null);

private void ReadHelper(Stream stream,int offset)

var buffer = new byte[BUFFER_LENGTH];

stream.BeginRead(buffer,offset,BUFFER_LENGTH,(ar) =>{

var actualRead = stream.EndRead(ar);

if(actualRead == BUFFER_LENGTH)

var partialContent = Encoding.Default.GetString(buffer);

Update(partialContent);

ReadHelper(stream,offset+BUFFER_LENGTH);

else

var latestContent = Encoding.Default.GetString(buffer,0,actualRead);

Update(latestContent);

stream.Close();

private void Update(string content)

this.BeginInvoke(new Action(()=>{this.txtContent.Text += content;}));

  除此之外,因為我們在這裡不能使用while等循環,我們想要從stream裡讀取完整的内容并不是一件容易事兒,我們必須将很好的循環結果替換成遞歸調用:ReadHelper。

  .NET Framework除了提供上面這種程式設計模式外,還提供了基于事件的異步程式設計模式。比如WebClient的很多方法就提供了異步版本,比如DownloadString方法。

  同步版本:

public string DownloadString(string url);

  異步版本:

public void DownloadStringAsync(string url);

public event DownloadStringCompleteEventHandler DownloadStringComplete;

  (在這裡請注意,這兩種異步程式設計模式以及未來要介紹的Async CTP中的TAP方法的命名,參數的傳遞都是有一定規則的,弄清楚這些規則在進行異步程式設計時會事半功倍)

基于事件的異步模式我也不作過多闡述,同樣可以參考《CLR via C#》以及MSDN。基于事件的異步程式設計模式點相比上一種的優點是實作了該模式的類一般從Component派生,是以可以獲得更好的設計器支援,但如此一來也會在性能上稍微差一點點。

  雖然微軟費盡心思,提出兩種異步程式設計的模式,讓我們編寫異步代碼能稍微輕松那麼一點點;但不管是使用回調還是基于事件的異步模式,都會将順序的同步方式的代碼拆成兩個部分:一個部分發起異步操作,而另外一個部分獲得結果。當有多個異步操作要進行時(比如上面的代碼首先使用異步的方式獲得response,然後又使用異步的方式讀取stream中的内容)就會回調裡嵌套着另外一個異步調用,代碼更加混亂。而且方法打散之後,像using、for、while、正常的異常處理都變得難以進行。代碼的可讀性也急劇降低,代碼又容易出錯,如是我們舍爾求其次,轉而去使用低效的同步版本。

  不過作為.NET程式員我們是幸運的,因為.NET提供的一些特性讓我們可以開發一些類庫輔助異步開發,比如Jeffrey Richter的AsyncEnumerator,以及微軟的CCR。我們會在接下來的文章裡讨論這些第三方類庫的使用以及背後的原理。

  《CLR via C#》

  關于IO部分,如果想更深入了解,可以使用IO完成端口(或對應英文IO Completion Port)進行搜尋