正確使用異步操做

本想寫一點有關LINQ to SQL異步調用的話題,可是在這以前我想仍是先寫一篇文章來闡述一下使用異步操做的一些原則,避免有些朋友誤用致使程序性能反而下降。這篇文章會討論一下在.NET中有關異步操做話題,從理論出發結合實際,以澄清概念及避免誤用爲目標,而且最後提出常見的異步操做場景和使用案例。這樣咱們就能夠知道何時該使用異步操做,何時會得不償失。數據庫

那麼咱們先來確認一個概念,那就是「線程」。請注意,若是沒有特殊說明,本文中出現的「線程」所指的是CLR線程池(Thread Pool)中的託管線程,它和Windows線程或纖程(fiber)並非同一個的概念。一樣,它也不是指System.Thread類的實例。簡單地說,它是由CLR管理的工做執行單元,每當須要執行任務時,CLR就會分配一個這樣的執行單元去工做。當全部的線程池內的線程都用完以後就沒法執行新的任務了,一個託管線程在任務完成以後被釋放爲止。線程池自己是一個「對象池」,會在須要新對象(託管線程)時建立,而在對象不須要以後(一段特定時間以內沒有新任務須要分配託管線程)負責銷燬以釋放資源。至於線程池的線程數量,在CLR 2.0 SP1以前的版本中是CPU數 * 25,不過從CLR 2.0 SP1以後就變成了CPU數 * 250。不過無論怎麼樣,線程池內的線程是有限的,咱們必須合理地使用它。編程

之前的計算機只有一個CPU,理論上同一時刻只能執行一個任務。而現在的超線程、多核、甚至是真正的多個CPU都使計算機可以同時運行多個任務。多線程編程的一個重要特色就是可以充分利用CPU的運算能力,更快地完成某個任務。很明顯,若是一個很是龐大的計算任務只交由一個線程來完成,那麼只能讓一個CPU參與運算。可是若是將一個大任務拆分紅多個互不影響的子任務,那麼就能讓多個CPU同時參與運算,所花的時間天然就少了。若是某個操做的目的是進行大量運算,或者說須要花費大量時間運算上的操做,咱們將其稱做「Compute-Bound Operation」,也就是受運算能力限制的操做。服務器

與「Compute-Bound Operation」相對的則是「IO-Bound Operation」。「IO-Bound Operation」是指那些因爲受到外部條件限制,完成這樣一個任務須要在IO上花費大量時間的操做。例如讀取一個文件,或者請求網絡上的某個資源。對於這種操做,計算的線程再多,運算能力再強也無濟於事,由於任務受到的是硬盤、網絡等IO設備帶來的限制。對於IO-Bound Operation,咱們能作的只有「等待」。網絡

對於「同步操做」來講,「等待」就意味着「阻塞」,一個線程將會「無所事事」直至操做完成。這種作法在許多時候會帶來各類問題,所以就出現了「異步操做」,可是一樣是「異步操做」,不一樣的任務,不一樣的狀況,它解決問題的方式和帶來的效果也是不一樣的。我下面就經過生活中的實例來講明這些內容:多線程

老趙的朋友開了一家餐館,請了10個工做人員。最近那個朋友常常向老趙抱怨,說工做人員人手老是不夠,在客人比較多的時候,老是來不及招呼他們。老趙一問才得知,這家餐館的工做方式比較特別:當客人來用餐時,就會有工做人員迎上去熱情招待,當客人點好菜以後,工做人員就會去進入廚房親自下廚——沒錯,就是這樣——作完以後,工做人員會將飯菜端至客人面前,而後就去招待別的客人。由於燒菜每每須要很長時間,所以在某些時候就會發現全部的工做人員都在廚房,可是卻沒有人點菜。因而老趙給朋友出了個主意:讓幾個工做人員做爲服務員,只負責招呼客人,剩下的就當廚師,一直在廚房工做。當客人點菜以後,服務員就把客人的需求告訴廚師,廚師開始工做,而服務員就能夠去招呼其餘客人了。朋友頓悟,問題就這樣迎刃而解了。異步

固然,上面故事中老趙的朋友實在太笨,現實生活中的餐館老闆都不會犯這種人員調度上的低級失誤。開發一個客戶端應用程序所遇到的狀況每每就和以上的狀況相似。在運行程序時,UI線程(服務員)負責顯示界面(招待客人),當用戶操做應用程序(點菜)以後,UI線程可使用同步操做進行運算(服務員親自下廚),可是若是這是個長時間的Compute-Bound Operation(燒菜是個花費人手時間較長的操做),界面就沒法重繪或響應用戶請求了(沒法招待客人了),這樣的應用程序用戶體驗天然很差(客人以爲服務質量低下)。可是隻要UI線程使用異步操做(通知廚師),讓另外一個線程(另外一個工做人員)來進行運算,UI線程就能夠繼續負責界面重繪或者其餘用戶操做(招待其餘客人)了。性能

在這種的狀況下,異步操做並無提升運算能力或者節省資源(仍是須要一我的員的工做),可是提供了較好的用戶體驗。不過咱們這時該怎麼利用異步操做呢?在實際開發中,咱們可使用委託的BeginInvoke進行異步調用。操作系統

下面的例子則對應了另外一種狀況:線程

老趙的那個開餐館的朋友在小賺一筆以後準備再開一家快餐店。快餐店和餐館有個不一樣之處,那就是快餐店的食品生產了大都有機器完成。惋惜在這種狀況下那個朋友仍是遇到了問題:機器數量綽綽有餘,可是人手仍是不夠。原來如今的作法仍是至關不科學:服務員知道客人須要的食品以後,就將原料塞入機器,並看着機器是如何將原料變爲美味的。當機器的工做完成以後,服務員便將食品打包並送出,而後繼續招待別的客人。老趙聽後仍是啼笑皆非:爲啥服務員不能在機器工做的時候就去招待別的客人呢?code

  與這個示例對應的能夠是一個ASP.NET應用程序。在ASP.NET中每一個請求(客人)都會使用一個線程池內的線程(服務員)來處理(招待),處理中極可能須要訪問數據庫(使用機器),對於普通的作法,處理線程會等待數據庫操做返回(服務員看着機器直至完成)。對於Web服務器來講,這極可能是個長時間的IO-Bound Operation,若是線程長時間被阻塞極可能就會下降Web應用程序的性能,由於線程池裏的線程用完以後(服務員都去看爐子了),就沒法處理新的請求了(沒人招待客人了)。若是咱們可以在數據庫進行長時間查詢操做時,讓線程去處理其餘的請求(招待其餘客人)。這樣,咱們只須要在數據庫操做完成以後繼續處理(打包)並將數據發送給客戶端(送出)便可。

這就是處理IO-Bound Operation的方式,很顯然,這也是一個異步操做。當咱們但願進行一個異步的IO-Bound Operation時,CLR會(經過Windows API)發出一個IRP(I/O Request Packet)。當設備準備穩當,就會找出一個它「最想處理」的IRP(例如一個讀取離當前磁頭最近的數據的請求)並進行處理,處理完畢後設備將會(經過Windows)交還一個表示工做完成的IRP。CLR會爲每一個進程建立一個IOCP(I/O Completion Port)並和Windows操做系統一塊兒維護。IOCP中一旦被放入表示完成的IRP以後(經過內部的ThreadPool.BindHandle完成),CLR就會盡快分配一個可用的線程用於繼續接下去的任務。

這種作法的須要一個重要條件,這就是發出用於請求的IRP的操做可以當即返回,而且這個IO操做不會使用任何線程。而此時,這種異步調用是真正地在節省資源,由於咱們能夠騰出線程用來處理其餘任務了,這就是和第一種異步調用的最大區別。不過很惋惜,這種作法顯然須要操做系統和設備的支持,也就是隻有特定的操做才能享受這些待遇。那麼.NET Framework中哪些操做能從中獲利呢?

  • FileStream操做:BeginRead、BeginWrite。調用BeginRead/BeginWrite時會發起一個異步操做,可是隻有在建立FileStream時傳入FileOptions.Asynchronous參數才能獲取真正的IOCP支持,不然BeginXXX方法將會使用默認定義在Stream基類上的實現。Stream基類中BeginXXX方法會使用委託的BeginInvoke方法來發起異步調用——這會使用一個額外的線程來執行任務。雖然當前調用線程當即返回了,可是數據的讀取或寫入操做依舊佔用着另外一個線程(IOCP支持的異步操做時不須要線程的),所以並無任何「節省」,反而還頗有可能下降了應用程序的性能,由於額外的線程切換會形成性能損失。
  • DNS操做:BeginGetHostByName、BeginResolve。
  • Socket操做:BeginAccept、BeginConnect、BeginReceive等等。
  • WebRequest操做:BeginGetRequestStream、BeginGetResponse。
  • SqlCommand操做:BeginExecuteReader、BeginExecuteNonQuery等等。這多是開發一個Web應用時最經常使用的異步操做了。若是須要在執行數據庫操做時獲得IOCP支持,那麼須要在鏈接字符串中標記Asynchronous Processing爲true(默認爲false),不然在調用BeginXXX操做時就會拋出異常。
  • WebServcie調用操做:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。

有一點我想再強調一下,那就是委託的BeginInvoke方法並不能得到IOCP支持,這會使用一個額外的線程來執行任務,這樣不但沒有節省,返而會下降性能。還有一點可能須要注意,IOCP的確能夠不佔用線程,可是一個真正的異步操做也不能毀在咱們的代碼中。例如我曾經看到過以下的代碼:

SqlCommand command;

IAsyncResult ar = command.BeginExecuteNonQuery();
int result = command.EndExecuteNonQuery(ar);

雖然在調用BeginExecuteNonQuery方法以後的確得到了IOCP的支持,可是以後調用的EndExecuteNonQuery卻會阻塞當前線程直至數據庫操做返回——異步操做不是這樣用的。至於正確的作法,網絡上已經有很多文章講述瞭如何在ASP.NET中正確使用異步操做,你們能夠搜索相應的資料來看,我也會在之後的文章中略有提到。

關於異步操做,此次就講到這裏吧。

相關文章
相關標籤/搜索