按照咱們常規的思惟方式,計算機應該是幹完一件事,而後再幹下一件。用術語來講,這種執行任務的方式叫作同步執行(Synchronous Execution)。既然這樣,那麼爲何要引入異步執行的概念呢?
目錄
爲何要使用異步調用
實現異步調用的步驟和機理
爲何要使用異步調用(Asynchronous Method Execution)
按照咱們常規的思惟方式,計算機應該是幹完一件事,而後再幹下一件。用術語來講,這種執行任務的方式叫作同步執行(Synchronous Execution)。既然這樣,那麼爲何要引入異步執行的概念呢?緣由很簡單,由於同步執行在有些狀況下效果不理想,不能完成咱們預期的目的。舉兩個簡單的例子來講明一下這個問題。
a. 一個客戶端程序(Client Side Program)要從後臺數據庫取回一個複雜的數據集合。可能這個數據庫操做自己很費時,也多是網絡傳輸的數度比較慢,總之這個方法調用可能要花20秒時間。若是使用同步調用,那麼在數據庫結果返回以前,用戶必須耐心等待,什麼也不能作。這時候你可能會但願這個調用慢慢的在別處進行,程序立刻返回好讓你作其它的工做。等何時數據返回了,在進行其隨後相應的操做。這種情形下,你就須要對數據庫操做的方法進行異步調用。
b.一個網上機票查詢訂閱程序。當客戶要查詢從北京到芝加哥的全部機票的時候,這個程序可能要在後臺經過Web Service對美國西北航空公司,中國國際航空公司和東方航空公司進行訪問。將這些公司的機票狀況彙總後一塊兒以HTML的形式返回給用戶。若是是同步調用,那麼須要一個接一個進行Web Service調用。若是每一個調用花費10秒鐘的話,那麼整個過程就要30秒鐘。若是你使用異步調用,那麼你能夠在同幾乎一時間就對三個公司發出相應的請求,10秒後當結果從三個不一樣的網站返回來後,你就能夠彙總並返回各用戶了。這樣,整個過程只須要10秒左右。
看到這裏你可能會說,這個問題沒什麼新鮮的。我在C++,Java裏均可以用線程(Thread)來達到這樣的效果。的確,大多數的高級語言都容許你建立新的線程來手工實現這樣的調用。可是這些手工操做比較複雜,程序員須要本身控制線程的建立,銷燬,協調等等許多細節工做,容易產生錯誤。而且在大型的服務器端的程序中,手工控制線程有時性能不夠優化,不能根據當前具體服務器的處理器狀況來動態的和智能的優化線程的數量。基於這個緣由,.NET建立了一種相對簡單的異步方法調用機制,使這一問題變得更加簡單。這就是今天要談的使用表明(Delegates)對方法進行異步調用。(本文以VB.NET來進行示範,C#的異步調用和此相似,就再也不給出例程了)
實現異步調用的步驟和機理
假設有這樣一個方法(Method),它接受一個班級的名稱,而後查詢數據庫,返回這個班級全部同窗的名單。
Class DemoClass
public shared Function GetStudentsList(ClassName as String)
as String()
'查詢數據庫
'其它操做
End function
End Class
若是對這樣一個方法進行異步調用的話,那麼你首先須要定義一個有一樣方法簽名(Function Signature)的表明(Delegate),好比
Delegate Function GetStudentListDelegate (ClassName as String) as String()
下一步,你須要生成一個表明實例(Instance),而後將這個表明和你的真正的方法「捆綁」起來,如
Dim delegate as GetStudentListDelegate
GetStudentListDelegate = AddressOf DemoClass.GetStudentsList
(爲了簡單起見,這裏使用了靜態方法,這其實不是必須的)
當你作到這步的時候,.NET的編譯器在後臺爲你的表明增長了幾個方法,它們是Invoke, BeginInvoke, EndInvoke.
若是你使用Invoke方法,那麼其效果是同步調用,好比
delegate.Invoke("class90")
在這種狀況下,表明將輸入參數"class90"傳遞給方法GetStudentsList,而後將這個方法的返回值返回給用戶。這種使用方法是同步的,不是咱們所期待的。若是要達到異步效果,咱們要使用BeginInvoke和EndInvoke。
讓咱們先看看BeginInvoke
你的使用方法可能以下所示:
Dim ar as System. IAsyncResult
ar = delegate.BeginInvoke("class90",Nothing, Nothing)
你可能會發現,這種調用方法有些不一樣。首先是多出兩個輸入參數,其次是返回值是System. IAsyncResult。這究竟是怎麼一回事呢?
當你調用BeginInvoke的時候,一系列的事情在後臺自動發生了。
當你用表明發出調用請求後,CLR(公共語言運行環境,Common Language Runtime)接到這個請求,並將這個請求放置到一個內部的處理隊列(Queue)中去。一旦放置完成後,CLR立刻就給調用者返回一個IAsyncResult的對象。這個對象很重要,咱們一下子還要解釋他的具體做用。
當調用者收到返回的IAsyncResult對象後,它就能夠進行下一步的工做。因爲將請求放置到隊列中是個很是快速的操做,因此調用者立刻就能夠去完成下一個動做,沒有被「阻擋(Block)」。
CLR在後臺維持着一個「線程池(Thread Pool)」。這些線程守候着前面提到的那個處理隊列。一旦有任務被放置到隊列中,一個線程就會拿到這個任務並執行它。也就是說原來要調用者線程執行的費時的操做被線程池中的一個線程代勞了。(這裏你能夠看出,不論是用什麼樣的語言,在異步調用中,必定有其它的線程出現。或者是你手工建立它(如Java),或者是系統爲你建立(如.NET)。那麼這個「線程池」中究竟有幾個線程呢?這個問題你能夠不用關心。CLR會根據程序的特色以及當前的硬件條件自行決定。好比對於運行在單處理器平臺上的通常的桌面程序,這個線程池可能有幾個線程;而對於一個運行在4處理器服務器上的後臺應用,線程池可能會有近百個線程。這樣作的好處就是下降程序員的開發難度,讓.NET的CLR去解決這些和用戶應用邏輯無關的問題。)
既然有線程池的線程代替完成了那個方法調用(GetStudentsList),那麼咱們怎麼知道後臺的這個調用何時完成呢?這個方法調用返回的值(這裏是一串學生名單)咱們怎麼拿到呢?這裏咱們就要用到前面提到的那個返回的IASyncResult對象了。 程序員
這個IASyncResult對象一個「收據」似的,經過它你能夠查詢後臺調用是否完成。若是已經完成,你能夠經過它來取回你想要的結果。
Dim ar as System.IASyncResult
ar = delegate.BeginInvoke("class90",Nothing, Nothing)
'*** 其它一些操做
。。。
'*** 檢查後臺調用狀態
If (ar.IsCompleted) Then
'*** 取回異步調用方法的結果
End If
若是後臺調用已經結束,那麼你就能夠用表明的EndInvoke來獲得返回值。
Dim Students as String()
Students = delegate.EndInvoke(ar)
那麼,若是你沒有測試後臺調用是否結束而直接使用EndInvoke,那後果會怎麼樣呢?若是後臺調用沒有完成,EndInvoke調用就會被「阻擋」,直到後臺調用完成後才返回。若是後臺調用出現異常,那麼EndInvoke還能夠捕捉到這個異常
Dim Students as String()
TryStudents = delegate.EndInvoke(ar)
Catch ex as Exception
'處理這個異常
End Try
既然EndInvoke調用就會被「阻擋」(若是後臺異步調用尚未完成),那麼下面這種標較複雜狀況CLR是怎樣處理的呢?
Dim ar1, ar2 as System.IASyncResult
Dim rt1, rt2 as String()
ar1 = delegate1.BeginInvoke("class90",Nothing, Nothing)
ar2 = delegate2.BeginInvoke("class94",Nothing, Nothing)
rt1 = delegate1.EndInvoke(ar1)
rt2 = delegate2.EndInvoke(ar2)
在這個例子中,delegate1的調用和delegate2的調用完成順序可能會有多種狀況。好比delegate2的調用後發先至,那麼EndInvoke的使用順序是否是很重要呢?事實上,你能夠忽略這個問題,CLR會保證在兩個異步調用都結束後,你才能夠進行下面的操做。至於它是怎麼實現的,你能夠不去管它。
事實上,EndInvoke是很是重要的。若是你使用了BeginInvoke,那你最好使用EndInvoke。由於你若是不使用EndInvoke,那麼後臺調用的異常就沒有機會被捕捉到。另外,使用了EndInvoke可讓CLR釋放異步調用中所使用的資源,不然你的應用程序就可能出現資源泄漏(Resource Leak)。
到這裏,狀況已經比較清楚了。使用Delegate可讓後臺線程代替當前線程去完成費時的操做,從而使當前線程不被「阻擋」,能夠立刻進行其它的工做。可是,若是當前線程經過EndInvoke來獲得異步調用的結果,它又極可能被「阻擋」。看起來有點「拆了東牆補西牆」的樣子,好像咱們沒有獲得什麼好處。打個比方來講吧,你要到複印室去複印一批材料,這個工做要費時一個多小時。同步調用就意味着你本身親自去複印,一個多小時候再返回辦公室做其它工做。異步調用意味着你能夠把複印材料交到複印室,那裏有專人負責複印。你放下材料後就能夠回到辦公室去幹其它工做了。但問題是,你要不停的查看材料是否複印好了,一旦發現複印完畢後,就立刻取回做相應的操做。你不停的查看(調用表明的IsComplete方法)或者是「乾等」(調用表明的EndInvoke方法)實際上仍是把你「捆住」了,你沒有能騰出手來幹其它的事。能不能我把材料放到複印室就無論了,等複印好後他們給我把材料送回來?。答案是能夠的,那就是利用回調函數(Callback Function)。
還記得咱們前面的那個例子嗎,咱們用表明調用BeginInvoke的時候,多了兩個參數,其中一個就是回調函數,另一個是執行回調函數的參數。回調函數的意思是在後臺線執行完異步調用的方法後,自動去執行的函數(或方法)。在執行這個回調函數的時候,你還能夠指定參數。也是就說,你讓複印室的複印員完成複印後,把材料給你放回到你的辦公桌上,而且每10頁一摞。這個「放到辦公桌上」就是回調函數,而「每10頁一摞」就是回調函數執行時使用的參數。
'回調函數的參數
Dim myValue as Integer = 10
'回調函數的定義
Sub PutToDesk(Byval ar as IAsyncResult)
dim x as Integer = CInt(ar.AsyncState)'拿到參數
'相應的操做
End Sub
'使用回調函數的方法
Private CallBackDelegate as AsyncCallBack = AddressOf PutToDesk
...
Dim ar as System.IASyncResult
ar = delegate.BeginInvoke("class90",CallBackDelegate, myValue)
在使用回調函數時要注意,你的回調函數必須和.NET系統定義的AsyncCallBack一塊兒使用,即你的回調函數必須和AsyncCallBack具備同樣的簽名。也就是說它必須是子程序(Sub Procedure),必須有一個IAsyncResult類對象爲輸入參數。
要注意的是回調函數是由後臺線程來執行的(就是咱們所說的複印員)。這種執行方法在有些狀況下會形成不小的問題。好比說,在Windows的桌面應用中有這樣一個規則,那就是一切用戶界面元素的更改(外觀以及屬性)必須由這些界面元素的建立線程來進行(術語上叫界面主線程,Primary UI Thread)。若是其它線程試圖更新界面元素,那麼將會有不可預測的後果。若是你違反了這一原則,那麼你的程序在理論上講是不安全的,即便是問題你一時尚未發現。
就上面一個例子而言,若是後臺線程從數據庫裏拿到了學生名單,那麼極可能它要執行的回調函數就是更新界面上的一個下拉式列表(Dropdown List),或是一個表格(Grid)什麼的。可是這樣作又違反了咱們所說的界面更新的線程原則。那麼咱們該怎麼辦呢?
其實這個問題並不難解決,設計師在設計.NET的時候已經考慮到了這個問題。數據庫