.NET中的異步編程(一)-為什么需要異步
在2010年的PDC上,微軟發布了Visual Studio Async CTP,大大地降低了異步編程的難度,讓我們可以像寫同步的方法那樣去編寫異步代碼。Async CTP也在社區里掀起了不小的波瀾。在這之后,我也學習了一段時間,這個系列會將這段時間的學習作個梳理。
好了,下面進入本文的正題。
為什么需要異步編程
既然同步的寫法更自然簡單,異步的代碼(傳統的)不好寫,還容易出錯,那我們為什么需要去編寫異步的代碼呢?微軟還要費這么大勁投入對Async CTP的開發呢?這其中肯定有一些原因。
快速響應的用戶界面
作為電腦的資深用戶,我們肯定有多次“漏斗式鼠標”,“轉圈式鼠標”的體驗吧。點擊一個按鈕,然后鼠標就在那兒不停的轉圈,再在界面上點兩下,界面變灰,標題欄上出現“沒有響應”。然后我們束手無策,性子好點的就在那兒等待一會兒,看看能不能恢復過來;性子不好的就打開任務管理器殺掉進程,殺掉進程容易,但有可能會破壞重要數據。
那造成這種情況到底是什么原因呢?概括成一句話就是:耗時的操作阻塞了UI線程,造成UI線程不能響應用戶操作。關于更底層的原因請移步我的這篇文章:WinForm二三事(一)消息循環。那么這個時候我們就需要一種機制,在發起耗時操作的請求之后要立即返回,不要阻塞UI線程,讓UI線程可以繼續響應用用戶的操作。然后等耗時操作返回后,通過回調來處理耗時操作返回的結果。下面是在UI上使用同步的方式和異步的方式的示意圖:
更高的伸縮性
對于服務器端應用來說,一般都是一個線程處理一個請求。另外一點是,線程的創建和銷毀是昂貴的(這一點可以參考《CLR via C#》中Thread Baisc一章的描述),而服務器的資源肯定是有限的;并且,線程創建的越多,線程上下文切換就會變得越頻繁。所以,為了創建高可伸縮性的服務,我們必須用最少的線程處理更多的請求,這樣不僅能夠做到消耗更少的資源(創建更少的線程),而且在應對請求突發增長的情況也很有用處,那么這里非常重要的一點就是不要阻塞線程,讓線程池能夠高效的工作。而且,在服務端應用中,有非常多的IO操作:數據庫訪問,磁盤操作,Socket訪問等。對于這些IO操作,不屬于計算密集型操作,是不需要單獨分配一個線程來處理的。
要做到高可伸縮性,異步是一劑良藥。假設現在這是一個web應用,當用戶的HTTP request到來時,線程池提供一個線程來處理(忽略前面的排隊等過程),然后到某一點,我們肯定需要讀取磁盤、訪問數據庫,這個時候我們使用異步的方式,發起IO請求,然后處理HTTP request的線程就可以返回到線程池了,它可以繼續處理其他請求,不需要在這里等待IO操作的返回。當IO操作完成之后,會通過回調(具體實現方式請參照后續文章)完成剛才那個HTTP reqeust后續的處理。
下面是使用同步方式和異步方式的示意圖:
上圖只畫出了一個請求,高亮顯示的那一段其實是不需要占用線程的,其實這段時間該線程可以返回線程池,然后分配去做其他請求,而數據庫返回結果之后,再從線程池里分配一個線程來處理后續操作。這樣,如果請求多的話,線程池就會創建更多的線程來處理請求,最后結果大家應該都知道了。
從上圖可以看出,開始的時候來自線程池的thread1處理請求,然后發起對數據庫的請求,發起操作完畢后,thread1被線程池回收;當數據庫將結果返回時線程池選擇另外一個線程thread2(有可能是原來的那個線程,如果空閑的話)來處理數據庫返回的結果,完成后續的操作。對于IO操作非常多的服務來說,所獲得的益處是不可估量的。
后記
本文主要從創建響應靈敏的用戶界面和創建高可伸縮性的服務應用這兩種不同的應用場景來闡釋我們為什么需要異步。至于如何進行異步開發在后續的文章我會首先介紹傳統的異步和Async CTP以及F#中的Async Workflow。