正確使用異步操作
2008-02-24 22:03 Jeffrey Zhao 閱讀(43643) 評論(40) 收藏 舉報本想寫一點有關(guān)LINQ to SQL異步調(diào)用的話題,但是在這之前我想還是先寫一篇文章來闡述一下使用異步操作的一些原則,避免有些朋友誤用導(dǎo)致程序性能反而降低。這篇文章會討論一下在.NET中有關(guān)異步操作話題,從理論出發(fā)結(jié)合實際,以澄清概念及避免誤用為目標,并且最后提出常見的異步操作場景和使用案例。這樣我們就可以知道什么時候該使用異步操作,什么時候會得不償失。
那么我們先來確認一個概念,那就是“線程”。請注意,如果沒有特殊說明,本文中出現(xiàn)的“線程”所指的是CLR線程池(Thread Pool)中的托管線程,它和Windows線程或纖程(fiber)并不是同一個的概念。同樣,它也不是指System.Thread類的實例。簡單地說,它是由CLR管理的工作執(zhí)行單元,每當(dāng)需要執(zhí)行任務(wù)時,CLR就會分配一個這樣的執(zhí)行單元去工作。當(dāng)所有的線程池內(nèi)的線程都用完之后就無法執(zhí)行新的任務(wù)了,一個托管線程在任務(wù)完成之后被釋放為止。線程池本身是一個“對象池”,會在需要新對象(托管線程)時創(chuàng)建,而在對象不需要之后(一段特定時間之內(nèi)沒有新任務(wù)需要分配托管線程)負責(zé)銷毀以釋放資源。至于線程池的線程數(shù)量,在CLR 2.0 SP1之前的版本中是CPU數(shù) * 25,不過從CLR 2.0 SP1之后就變成了CPU數(shù) * 250。不過不管怎么樣,線程池內(nèi)的線程是有限的,我們必須合理地使用它。
以前的計算機只有一個CPU,理論上同一時刻只能執(zhí)行一個任務(wù)。而如今的超線程、多核、甚至是真正的多個CPU都使計算機能夠同時運行多個任務(wù)。多線程編程的一個重要特點就是能夠充分利用CPU的運算能力,更快地完成某個任務(wù)。很明顯,如果一個非常龐大的計算任務(wù)只交由一個線程來完成,那么只能讓一個CPU參與運算。但是如果將一個大任務(wù)拆分成多個互不影響的子任務(wù),那么就能讓多個CPU同時參與運算,所花的時間自然就少了。如果某個操作的目的是進行大量運算,或者說需要花費大量時間運算上的操作,我們將其稱作“Compute-Bound Operation”,也就是受運算能力限制的操作。
與“Compute-Bound Operation”相對的則是“IO-Bound Operation”。“IO-Bound Operation”是指那些由于受到外部條件限制,完成這樣一個任務(wù)需要在IO上花費大量時間的操作。例如讀取一個文件,或者請求網(wǎng)絡(luò)上的某個資源。對于這種操作,計算的線程再多,運算能力再強也無濟于事,因為任務(wù)受到的是硬盤、網(wǎng)絡(luò)等IO設(shè)備帶來的限制。對于IO-Bound Operation,我們能做的只有“等待”。
對于“同步操作”來說,“等待”就意味著“阻塞”,一個線程將會“無所事事”直至操作完成。這種做法在許多時候會帶來各種問題,因此就出現(xiàn)了“異步操作”,但是同樣是“異步操作”,不同的任務(wù),不同的情況,它解決問題的方式和帶來的效果也是不同的。我下面就通過生活中的實例來說明這些內(nèi)容:
老趙的朋友開了一家餐館,請了10個工作人員。最近那個朋友經(jīng)常向老趙抱怨,說工作人員人手總是不夠,在客人比較多的時候,總是來不及招呼他們。老趙一問才得知,這家餐館的工作方式比較特別:當(dāng)客人來用餐時,就會有工作人員迎上去熱情招待,當(dāng)客人點好菜之后,工作人員就會去進入廚房親自下廚——沒錯,就是這樣——做完之后,工作人員會將飯菜端至客人面前,然后就去招待別的客人。因為燒菜往往需要很長時間,因此在某些時候就會發(fā)現(xiàn)所有的工作人員都在廚房,但是卻沒有人點菜。于是老趙給朋友出了個主意:讓幾個工作人員作為服務(wù)員,只負責(zé)招呼客人,剩下的就當(dāng)廚師,一直在廚房工作。當(dāng)客人點菜之后,服務(wù)員就把客人的需求告訴廚師,廚師開始工作,而服務(wù)員就可以去招呼其他客人了。朋友頓悟,問題就這樣迎刃而解了。
當(dāng)然,上面故事中老趙的朋友實在太笨,現(xiàn)實生活中的餐館老板都不會犯這種人員調(diào)度上的低級失誤。開發(fā)一個客戶端應(yīng)用程序所遇到的情況往往就和以上的情況類似。在運行程序時,UI線程(服務(wù)員)負責(zé)顯示界面(招待客人),當(dāng)用戶操作應(yīng)用程序(點菜)之后,UI線程可以使用同步操作進行運算(服務(wù)員親自下廚),但是如果這是個長時間的Compute-Bound Operation(燒菜是個花費人手時間較長的操作),界面就無法重繪或響應(yīng)用戶請求了(無法招待客人了),這樣的應(yīng)用程序用戶體驗自然不好(客人覺得服務(wù)質(zhì)量低下)。但是只要UI線程使用異步操作(通知廚師),讓另一個線程(另一個工作人員)來進行運算,UI線程就可以繼續(xù)負責(zé)界面重繪或者其他用戶操作(招待其他客人)了。
在這種的情況下,異步操作并沒有提高運算能力或者節(jié)省資源(還是需要一個人員的工作),但是提供了較好的用戶體驗。不過我們這時該怎么利用異步操作呢?在實際開發(fā)中,我們可以使用委托的BeginInvoke進行異步調(diào)用。
下面的例子則對應(yīng)了另一種情況:
老趙的那個開餐館的朋友在小賺一筆之后準備再開一家快餐店。快餐店和餐館有個不同之處,那就是快餐店的食品生產(chǎn)了大都有機器完成。可惜在這種情況下那個朋友還是遇到了問題:機器數(shù)量綽綽有余,但是人手還是不夠。原來現(xiàn)在的做法還是相當(dāng)不科學(xué):服務(wù)員知道客人需要的食品之后,就將原料塞入機器,并看著機器是如何將原料變?yōu)槊牢兜摹.?dāng)機器的工作完成之后,服務(wù)員便將食品打包并送出,然后繼續(xù)招待別的客人。老趙聽后還是哭笑不得:為啥服務(wù)員不能在機器工作的時候就去招待別的客人呢?
與這個示例對應(yīng)的可以是一個ASP.NET應(yīng)用程序。在ASP.NET中每個請求(客人)都會使用一個線程池內(nèi)的線程(服務(wù)員)來處理(招待),處理中很可能需要訪問數(shù)據(jù)庫(使用機器),對于普通的做法,處理線程會等待數(shù)據(jù)庫操作返回(服務(wù)員看著機器直至完成)。對于Web服務(wù)器來說,這很可能是個長時間的IO-Bound Operation,如果線程長時間被阻塞很可能就會降低Web應(yīng)用程序的性能,因為線程池里的線程用完之后(服務(wù)員都去看爐子了),就無法處理新的請求了(沒人招待客人了)。如果我們能夠在數(shù)據(jù)庫進行長時間查詢操作時,讓線程去處理其他的請求(招待其他客人)。這樣,我們只需要在數(shù)據(jù)庫操作完成之后繼續(xù)處理(打包)并將數(shù)據(jù)發(fā)送給客戶端(送出)即可。
這就是處理IO-Bound Operation的方式,很顯然,這也是一個異步操作。當(dāng)我們希望進行一個異步的IO-Bound Operation時,CLR會(通過Windows API)發(fā)出一個IRP(I/O Request Packet)。當(dāng)設(shè)備準備妥當(dāng),就會找出一個它“最想處理”的IRP(例如一個讀取離當(dāng)前磁頭最近的數(shù)據(jù)的請求)并進行處理,處理完畢后設(shè)備將會(通過Windows)交還一個表示工作完成的IRP。CLR會為每個進程創(chuàng)建一個IOCP(I/O Completion Port)并和Windows操作系統(tǒng)一起維護。IOCP中一旦被放入表示完成的IRP之后(通過內(nèi)部的ThreadPool.BindHandle完成),CLR就會盡快分配一個可用的線程用于繼續(xù)接下去的任務(wù)。
這種做法的需要一個重要條件,這就是發(fā)出用于請求的IRP的操作能夠立即返回,并且這個IO操作不會使用任何線程。而此時,這種異步調(diào)用是真正地在節(jié)省資源,因為我們可以騰出線程用來處理其他任務(wù)了,這就是和第一種異步調(diào)用的最大區(qū)別。不過很可惜,這種做法顯然需要操作系統(tǒng)和設(shè)備的支持,也就是只有特定的操作才能享受這些待遇。那么.NET Framework中哪些操作能從中獲利呢?
- FileStream操作:BeginRead、BeginWrite。調(diào)用BeginRead/BeginWrite時會發(fā)起一個異步操作,但是只有在創(chuàng)建FileStream時傳入FileOptions.Asynchronous參數(shù)才能獲取真正的IOCP支持,否則BeginXXX方法將會使用默認定義在Stream基類上的實現(xiàn)。Stream基類中BeginXXX方法會使用委托的BeginInvoke方法來發(fā)起異步調(diào)用——這會使用一個額外的線程來執(zhí)行任務(wù)。雖然當(dāng)前調(diào)用線程立即返回了,但是數(shù)據(jù)的讀取或?qū)懭氩僮饕琅f占用著另一個線程(IOCP支持的異步操作時不需要線程的),因此并沒有任何“節(jié)省”,反而還很有可能降低了應(yīng)用程序的性能,因為額外的線程切換會造成性能損失。
- DNS操作:BeginGetHostByName、BeginResolve。
- Socket操作:BeginAccept、BeginConnect、BeginReceive等等。
- WebRequest操作:BeginGetRequestStream、BeginGetResponse。
- SqlCommand操作:BeginExecuteReader、BeginExecuteNonQuery等等。這可能是開發(fā)一個Web應(yīng)用時最常用的異步操作了。如果需要在執(zhí)行數(shù)據(jù)庫操作時得到IOCP支持,那么需要在連接字符串中標記Asynchronous Processing為true(默認為false),否則在調(diào)用BeginXXX操作時就會拋出異常。
- WebServcie調(diào)用操作:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。
有一點我想再強調(diào)一下,那就是委托的BeginInvoke方法并不能獲得IOCP支持,這會使用一個額外的線程來執(zhí)行任務(wù),這樣不但沒有節(jié)省,返而會降低性能。還有一點可能需要注意,IOCP的確可以不占用線程,但是一個真正的異步操作也不能毀在我們的代碼中。例如我曾經(jīng)看到過如下的代碼:
SqlCommand command;
IAsyncResult ar = command.BeginExecuteNonQuery();
int result = command.EndExecuteNonQuery(ar);
雖然在調(diào)用BeginExecuteNonQuery方法之后的確獲得了IOCP的支持,但是之后調(diào)用的EndExecuteNonQuery卻會阻塞當(dāng)前線程直至數(shù)據(jù)庫操作返回——異步操作不是這樣用的。至于正確的做法,網(wǎng)絡(luò)上已經(jīng)有不少文章講述了如何在ASP.NET中正確使用異步操作,大家可以搜索相應(yīng)的資料來看,我也會在以后的文章中略有提到。
關(guān)于異步操作,這次就講到這里吧。
浙公網(wǎng)安備 33010602011771號