VB.NET中使用代表對方法非同步呼叫

來源:互聯網
上載者:User
為什麼要使用非同步呼叫(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()
Try Students = 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的時候已經考慮到了這個問題。具體的解決辦法我將在下篇文章中做出介紹。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.