VB.NET中使用代表對方法異步調用


按照我們常規的思維方式,計算機應該是干完一件事,然后再干下一件。用術語來說,這種執行任務的方式叫做同步執行(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的時候已經考慮到了這個問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM