.NET中的異步編程(二):傳統(tǒng)的異步編程
上一篇文章中,我們討論了為什么需要異步編程,異步編程能給我們帶來(lái)哪些好處。這篇文章是要介紹傳統(tǒng)的一步編程Async CTP。
如何實(shí)現(xiàn)異步
對(duì)于很多人來(lái)說(shuō),異步就是使用后臺(tái)線程運(yùn)行耗時(shí)的操作。在有些時(shí)候這是對(duì)的,而在我們?nèi)粘4蟛糠謭?chǎng)景中卻不對(duì)。
比如現(xiàn)在我們有這么一個(gè)需求:使用HttpWebRequest請(qǐng)求某個(gè)指定URI的內(nèi)容,然后輸出在界面上的文本域中。同步代碼很容易編寫:
- 1: private void btnDownload_Click(object sender,EventArgs e)
- 2: {
- 3: var request = HttpWebRequest.Create("http://www.sina.com.cn");
- 4: var response = request.GetResponse();
- 5: var stream = response.GetResponseStream();
- 6: using(StreamReader reader = new StreamReader(stream))
- 7: {
- 8: var content = reader.ReadToEnd();
- 9: this.txtContent.Text = content;
- 10: }
- 11: }
是吧,很簡(jiǎn)單。但是正如上一篇文章所說(shuō),這個(gè)簡(jiǎn)短的程序體驗(yàn)會(huì)非常差。特別是在URI所指向的資源非常大,網(wǎng)絡(luò)非常慢的情況下,在點(diǎn)擊下載按鈕到獲得結(jié)果這段時(shí)間界面會(huì)假死。
哦,這個(gè)時(shí)候你想起了異步。回憶上篇文章的示意圖。我們發(fā)現(xiàn)只要我們將耗時(shí)的操作放到另外一個(gè)線程上執(zhí)行就可以了,這樣我們的UI線程可以繼續(xù)響應(yīng)用戶的操作。
使用獨(dú)立的線程實(shí)現(xiàn)異步
如是你寫下了下面的代碼:
- 1: private void btnDownload_Click(object sender,EventArgs e)
- 2: {
- 3: var downloadThread = new Thread(Download);
- 4: downloadThread.Start();
- 5: }
- 6:
- 7: private void Download()
- 8: {
- 9: var request = HttpWebRequest.Create("http://www.sina.com.cn");
- 10: var response = request.GetResponse();
- 11: var stream = response.GetResponseStream();
- 12: using(StreamReader reader = new StreamReader(stream))
- 13: {
- 14: var content = reader.ReadToEnd();
- 15: this.txtContent.Text = content;
- 16: }
- 17: }
然后,F(xiàn)5運(yùn)行。很不幸,這里出現(xiàn)了異常:我們不能在一個(gè)非UI線程上更新UI的屬性,我們暫時(shí)忽略這個(gè)異常(在release模式下是不會(huì)出現(xiàn)的,但這是不推薦的做法)。
哦,你寫完上面的代碼后發(fā)現(xiàn)UI不再阻塞了。心里想,異步也不過(guò)如此嘛。過(guò)了一會(huì)兒你突然想起,你好像在哪本書里看到過(guò)說(shuō)盡量不要自己聲明Thread,而應(yīng)用使用線程池。如是你搜索了一下MSDN,將上面的代碼改成下面這個(gè)樣子:
- 1: private void btnDownload_Click(object sender,EventArgs e)
- 2: {
- 3: ThreadPool.QueueUserWorkItem(Download);
- 4: }
- 5:
- 6: private void Download()
- 7: {
- 8: var request = HttpWebRequest.Create("http://www.sina.com.cn");
- 9: var response = request.GetResponse();
- 10: var stream = response.GetResponseStream();
- 11: using(StreamReader reader = new StreamReader(stream))
- 12: {
- 13: var content = reader.ReadToEnd();
- 14: this.txtContent.Text = content;
- 15: }
- 16: }
嗯,很容易完成了。你都有點(diǎn)佩服自己了,這么短的時(shí)間居然連線程池這么“高級(jí)的技術(shù)”都給使用上了。就在你沾沾自喜的時(shí)候,你的一個(gè)同事走過(guò)來(lái)說(shuō):你這種實(shí)現(xiàn)方式是非常低效的,這里要進(jìn)行的耗時(shí)操作屬于IO操作,不是計(jì)算密集型,可以不分配線程給它(雖然不算準(zhǔn)確,但如果不深究的話就這么認(rèn)為吧)。
你的同事說(shuō)的是對(duì)的。對(duì)于IO操作(比如讀寫磁盤,網(wǎng)絡(luò)傳輸,數(shù)據(jù)庫(kù)查詢等),我們是不需要占用一個(gè)thread來(lái)執(zhí)行的?,F(xiàn)代的磁盤等設(shè)備,都可以與CPU同時(shí)工作,在磁盤尋道讀取這段時(shí)間CPU可以干其他的事情,當(dāng)讀取完畢之后通過(guò)中斷再讓CPU參與進(jìn)來(lái)。所以上面的代碼,雖然構(gòu)建了響應(yīng)靈敏的界面,但是卻創(chuàng)建了一個(gè)什么也不干的線程(當(dāng)進(jìn)行網(wǎng)絡(luò)請(qǐng)求這段時(shí)間內(nèi),該線程會(huì)被一直阻塞)。所以,如果你要進(jìn)行異步時(shí)首先要考慮,耗時(shí)的操作屬于計(jì)算密集型還是IO密集型,不同的操作需要采用不同的策略。對(duì)于計(jì)算密集型的操作你是可以采用上面的方法的:比如你要進(jìn)行很復(fù)雜的方程的求解。是采用專門的線程還是使用線程池,也要看你的操作的關(guān)鍵程度。
這個(gè)時(shí)候你又在思考,不讓我使用線程,又要讓我實(shí)現(xiàn)異步。這該怎么辦呢?微軟早就幫你想到了這點(diǎn),在.NET Framework中,幾乎所有進(jìn)行IO操作的方法幾乎都提供了同步版本和異步版本,而且微軟為了簡(jiǎn)化異步的使用難度還定義了兩種異步編程模式:
Classic Async Pattern
這種方式就是提供兩個(gè)方法實(shí)現(xiàn)異步編程:比如System.IO.Stream的Read方法:
public int Read(byte[] buffer,int offset,int count);
它還提供了兩個(gè)方法實(shí)現(xiàn)異步讀?。?/P>
public IAsyncResult BeginRead(byte[] buffer, int offset,int count,AsyncCallback callback);
public int EndRead(IAsyncResult asyncResult);
以Begin開頭的方法發(fā)起異步操作,Begin開頭的方法里還會(huì)接收一個(gè)AsyncCallback類型的回調(diào),該方法會(huì)在異步操作完成后執(zhí)行。然后我們可以通過(guò)調(diào)用EndRead獲得異步操作的結(jié)果。關(guān)于這種模式更詳細(xì)的細(xì)節(jié)我不在這里多闡述,感興趣的同學(xué)可以閱讀《CLR via C#》26、27章,以及《.NET設(shè)計(jì)規(guī)范》里對(duì)異步模式的描述。在這里我會(huì)使用這種模式重新實(shí)現(xiàn)上面的代碼片段:
- 1: private static readonly int BUFFER_LENGTH = 1024;
- 2:
- 3: private void btnDownload_Click(object sender,EventArgs e)
- 4: {
- 5: var request = HttpWebRequest.Create("http://www.sina.com.cn");
- 6: request.BeginGetResponse((ar) => {
- 7: var response = request.EndRequest(ar);
- 8: var stream = response.GetResponseStream();
- 9: ReadHelper(stream,0);
- 10: },null);
- 11: }
- 12:
- 13: private void ReadHelper(Stream stream,int offset)
- 14: {
- 15: var buffer = new byte[BUFFER_LENGTH];
- 16: stream.BeginRead(buffer,offset,BUFFER_LENGTH,(ar) =>{
- 17: var actualRead = stream.EndRead(ar);
- 18:
- 19: if(actualRead == BUFFER_LENGTH)
- 20: {
- 21: var partialContent = Encoding.Default.GetString(buffer);
- 22: Update(partialContent);
- 23: ReadHelper(stream,offset+BUFFER_LENGTH);
- 24: }
- 25: else
- 26: {
- 27: var latestContent = Encoding.Default.GetString(buffer,0,actualRead);
- 28: Update(latestContent);
- 29: stream.Close();
- 30: }
- 31: },null);
- 32: }
- 33:
- 34: private void Update(string content)
- 35: {
- 36: this.BeginInvoke(new Action(()=>{this.txtContent.Text += content;}));
- 37: }
感謝lambda表達(dá)式,讓我少些了很多方法聲明,也少引入了很多實(shí)例成員。不過(guò)上面的代碼還是非常難以讀懂,原本簡(jiǎn)簡(jiǎn)單單的同步代碼被改寫成了分段式的,而且我們?cè)僖矡o(wú)法使用using了,所以需要顯示的寫stream.Close()。哦,我的代碼還沒有進(jìn)行異常處理,這令我非常頭痛。實(shí)際上要寫出一個(gè)健壯的異步代碼是非常困難的,而且非常難以調(diào)試。但是,上面的代碼不僅僅能創(chuàng)建響應(yīng)靈敏的界面,還能更高效的利用線程。在這種異步模式中,BeginXXX方法會(huì)返回一個(gè)IAsyncResult對(duì)象,在進(jìn)行異步編程時(shí)也非常有效。
除此之外,因?yàn)槲覀冊(cè)谶@里不能使用while等循環(huán),我們想要從stream里讀取完整的內(nèi)容并不是一件容易事兒,我們必須將很好的循環(huán)結(jié)果替換成遞歸調(diào)用:ReadHelper。
Event-based Async Pattern(EAP)
.NET Framework除了提供上面這種編程模式外,還提供了基于事件的異步編程模式。比如WebClient的很多方法就提供了異步版本,比如DownloadString方法。
同步版本:
public string DownloadString(string url);
異步版本:
public void DownloadStringAsync(string url);
public event DownloadStringCompleteEventHandler DownloadStringComplete;
(在這里請(qǐng)注意,這兩種異步編程模式以及未來(lái)要介紹的Async CTP中的TAP方法的命名,參數(shù)的傳遞都是有一定規(guī)則的,弄清楚這些規(guī)則在進(jìn)行異步編程時(shí)會(huì)事半功倍)
基于事件的異步模式我也不作過(guò)多闡述,同樣可以參考《CLR via C#》以及MSDN?;谑录漠惒骄幊棠J近c(diǎn)相比上一種的優(yōu)點(diǎn)是實(shí)現(xiàn)了該模式的類一般從Component派生,所以可以獲得更好的設(shè)計(jì)器支持,但如此一來(lái)也會(huì)在性能上稍微差一點(diǎn)點(diǎn)。
尷尬
雖然微軟費(fèi)盡心思,提出兩種異步編程的模式,讓我們編寫異步代碼能稍微輕松那么一點(diǎn)點(diǎn);但不管是使用回調(diào)還是基于事件的異步模式,都會(huì)將順序的同步方式的代碼拆成兩個(gè)部分:一個(gè)部分發(fā)起異步操作,而另外一個(gè)部分獲得結(jié)果。當(dāng)有多個(gè)異步操作要進(jìn)行時(shí)(比如上面的代碼首先使用異步的方式獲得response,然后又使用異步的方式讀取stream中的內(nèi)容)就會(huì)回調(diào)里嵌套著另外一個(gè)異步調(diào)用,代碼更加混亂。而且方法打散之后,像using、for、while、常規(guī)的異常處理都變得難以進(jìn)行。代碼的可讀性也急劇降低,代碼又容易出錯(cuò),如是我們舍爾求其次,轉(zhuǎn)而去使用低效的同步版本。
不過(guò)作為.NET程序員我們是幸運(yùn)的,因?yàn)?NET提供的一些特性讓我們可以開發(fā)一些類庫(kù)輔助異步開發(fā),比如Jeffrey Richter的AsyncEnumerator,以及微軟的CCR。我們會(huì)在接下來(lái)的文章里討論這些第三方類庫(kù)的使用以及背后的原理。
最后還是套用Async CTP的程序經(jīng)理Lucian Wischik的那句話:異步并不意味著后臺(tái)線程結(jié)束本文。
參考文獻(xiàn)
《CLR via C#》
【編輯推薦】
- .NET中的異步編程(一):異步編程的原因
- .Net reflector兩款神器 Deblector和reflexil
- 淺談ASP.NET MVC 3中如何使用Model
- 手把手教你實(shí)現(xiàn).NET程序打包