不管是使用yield或借助第三方類庫來簡化異步程式設計,或多或少總是感覺不那麼正統,有點hack的感覺。這種感覺在實驗階段倒還可以,要是用在産品中總有點擔心,即使這些類庫來自權威的第三方,我不知道大家有沒有跟我同樣的感覺。那麼這個時候我們就會想,如果在語言中直接能提供這種機制該多好呢。
F#的異步工作流
在Visual Studio 2010中,新包含了一種語言:F#。F#的一大特性就是異步計算。能讓你用同步的方式編寫異步的代碼,不用使用AsyncCallback回調将一個方法分為兩段,也不用注冊異步完成事件。
F#是一個強類型的函數式程式設計語言,現在是2.0版本,在VS2010中正式作為first-class語言出現。其主要設計者是Don Syme,同是.NET中的泛型的主要設計者之一。
我們來看看前面幾篇文章中都包含的那個示例使用F#的代碼将是怎樣:
let asyncDownload (url:string) =
async{
let req = WebRequest.Create(url)
let! resp = req.AsyncGetResponse()
use stream
= resp.GetResponseStream()
let reader = new StreamReader(stream)
return reader.ReadToEnd()
}
很短小精悍吧(實際上這段代碼可以更短,但為了說明異步的編寫方式,我沒有使用那些看起來有點怪的文法)。下面我們來解讀一下這段代碼,希望本文結束後你能對F#中的異步有點初步的印象。
F#中用let定義一個值,比如:
let value = 5
不過上面的代碼是用let定義一個函數。不過請記住,F#是函數式程式設計語言,在這裡函數也是值。觀察上面的代碼,let後面是函數名,然後跟着函數的參數清單(url : string)。F#有強大的類型推斷能力,一般情況下參數的類型是不需要寫的,但是在這個代碼中因為WebRequest.Create方法有多個重載方式,是以無法推斷出url的類型,需要加上string。注意這裡定義類型的方式。
下面是本問最有趣的地方了:async{…}。這就是所謂的異步工作流,它實際上是AsyncBuilder的一個全局示例。而被包括在async{}内的一些操作被轉換為FSharpAsyncBuilder的方法調用,這種使用方式在F#裡稱之為工作流(Workflow)。注意,這和業務系統中的工作流程系統是不同的。關于工作流我會在下一篇文章中詳細介紹。現在我們隻需要知道async{}執行的結果放回的是一個Asynca(泛型)類型的東西,這個東西表示一個可以被排程運作的任務,我們可以這樣使用這個方法:
Async.RunSynchronously(asyncDownload("http://www.google.com"))
Async.RunSynchronously會将asyncDownload傳回的這個異步任務放到線程池中執行,然後阻塞線程等待傳回結果。
在這裡需要說明的是,前面下載下傳網頁内容的代碼是異步執行的。當調用req.AsyncGetResponse時隻是注冊異步回調,但是并不阻塞線程。當從遠端伺服器拿到響應後會接着執行req.AsyncGetResponse後面的内容。這樣就用非常自然的同步順序的方式,編寫出異步的代碼。這比之前使用各種第三方類庫的hack方式看起來更自然。至于這種方式背後的實作原理,敬請期待下篇文章。
但這裡存在一個問題是,如果我們現在的場景是這樣的:
在Winform窗體上有一個按鈕,點選按鈕後下載下傳網頁内容,然後将内容顯示在頁面上的一個文本框中。那麼這裡使用Async.RunSynchronously就不是個好注意。RunSynchronously會阻塞UI線程直到傳回結果,那麼我們在本系列開始的時候提到建構靈敏的UI的目的就沒有達到。我們不能阻塞UI線程,應該讓它執行完畢後調用我們的代碼将内容顯示在文本框中。幸好Async提供了StartWithContinuations方法,我們就可以這樣來執行我們的下載下傳程式了:
Async.StartWithContinuations(asyncDownload("http://www.google.com",
(func html
- textBox.Text = html),
(func ex
- MessageBox.Show(ex.Message))
(func canl
- MessageBox.Show("canceled"))
Async.StartWithContinuations第一個參數接收的是我們異步執行的任務,然後接收三個回調。這三個回調就是我在.NET中的異步程式設計(三)- Continuation passing style以及使用yield實作異步中介紹的continuation。它們分别是成功執行後将怎樣(在textBox中顯示),發生異常後怎樣(使用MessageBox顯示警告),以及任務被取消了将怎樣。使用這個後這裡就不再有阻塞了,continuation是在asyncDownload執行完畢後觸發的,不是通過阻塞線程等來的。
實際上在textBox中顯示html源代碼這個回調不再是在UI線程上工作了,聰明的你應該知道這樣會抛出不在建立控件的線程上修改控件的屬性的異常,但是奇怪的是這裡卻沒有抛出這個異常,這都是StartWithContinuations的功勞,它在開始運作的時候會捕獲目前程式的上下文(SynchronizationContext),然後在執行continuations的時候會在原來捕獲的上下文中執行代碼,是以也就不會抛出那個異常了。
req.AsyncGetResponse的實作:
在這裡我還想介紹的就是req.AsyncGetResponse,其實作思路在異步程式設計封裝裡經常使用,在Async CTP中也提供了類似的擴充類庫。AsyncGetResponse方法是一個WebRequest的擴充方法,在内部調用Async.FromBeginEnd封裝WebRequest的BeginGetResponse和EndGetResponse。其大概想法就是這樣的:
//以下都不是真實代碼,僅為了闡明思路
publicstatic Async AsyncGetResponse(this WebRequest request)
{
return Async.FromBeginEnd(request.BeginGetResponse,request.EndGetResponse,request);
}
//下面是Async類的FromBeginEnd方法僞代碼
publicstatic Async FromBeginEnd(Func beginAction,Func endAction,WebRequest request)
//建立一個async,類似一個工作項或Task
Async async
= ...
IAsyncResult ar
= null;
AsyncCallback callback
= (state) =
{
WebResponse response
= endAction(ar);
//當給async這個任務設定了結果後就表明該任務執行完畢了,它以後的任務可以接着執行了
async.Result
= response;
};
ar
= beginAction(callback,request);
return async;
我們可以将Async所代表的東西當作一個可以在未來某個時刻獲得結果的任務,它現在還在執行。等到它的Result被設定的時候該任務就會完成,完成後就會觸發一個事件,我們要做的就是注冊這個事件,然後在事件發生後我們接着執行req.AsyncGetResponse的代碼(如何實作是工作流的功勞)。
F#中還提供了對其他異步方法的擴充,思路跟這裡差不多,都可以使用AsyncXXX的方法調用。
這裡需要注意的是,因為Asynca僅僅表示一個未來可獲得結果的任務,是以它内部不一定必須包含異步的操作。我們甚至可以将一段計算密集型的代碼放到它内部(比如解一個方程),它就僅僅提供了一個排程的單元,更好的組織我們的代碼。比如下面這樣的代碼:
Async.Parallel [ async{//長時間運作的任務1}; async{//長時間運作的任務2}....]
我們利用async{}建立很多運算單元,然後利用Async.Parallel方法并行的執行這些計算單元。這樣的代碼比零散執行的代碼更容易讀,就像建立了很多對象來表示這些計算單元一樣。
總結:
本文隻是簡單的介紹了下F#中編寫異步程式設計的方法。我希望讀者看完本文後能建立這樣一些概念:
1、上面代碼中async{}内的代碼是異步執行的,沒有線程被阻塞,即使是通路非常慢的遠端伺服器時。
2、async傳回的Asynca代表一個未來可以得到結果 a 的任務。
3、利用這種FromBeginEnd封裝傳統的異步程式設計的BeginXXX和EndXXX方法的方式是提供異步擴充庫的常用做法。