F#與ASP.NET(2):使用F#實現基於事件的非同步模式
在上一篇文章中,我們的簡單討論了.NET中兩種非同步模型以及它們在異常處理上的區別,並且簡單觀察了ASP.NET MVC 2中非同步Action的編寫方式。從中我們得知,ASP.NET MVC 2的非同步Action並非使用了傳統基於Begin/End的非同步程式設計模型,而是另一種基於事件的非同步模式。此外,ASP.NET MVC 2對於這種非同步模式提供了必要的支援,使此方面的程式設計變得相對簡單一些。但是,簡單的原因主要還是在於已經由其他元件提供了良好的,基於事件的非同步模式。那麼現在我們就來看看一般我們應該如何來實現這樣的功能,以及F#是如何美化我們的生活的吧。
非同步資料傳輸
我們為什麼要非同步,主要目的之一還是提高I/O操作的伸縮性。I/O操作主要是I/O裝置的事情,這些裝置在好好工作的時候,是不需要系統花心思進行照看的,它們只要能夠在完成指定工作後通知系統就可以了。這也是非同步I/O高效的原理,因為它將程式從等待I/O完成的苦悶中解脫出來,這對使用者或是系統來說都是一件絕好的事情。
那麼說到I/O操作,最典型的場景之一便是資料傳輸了。比如有兩個資料流streamIn和streamOut,我們需要非同步地從streamIn中讀取資料,並非同步地寫入到streamOut中。在這個過程中,我們使用一個相對較小的byte陣列作為快取空間,這樣程式在進行資料傳輸時便不會佔用太多記憶體。那麼,如果現在需要您編寫一個元件完成這樣的資料傳輸工作,並使用標準的基於事件的非同步模式釋放出來,您會怎麼做?
再囉嗦一次,基於事件的非同步模式,要求在任務完成時使用事件進行提示。同時在出錯的時候將異常物件儲存在事件的引數中。現在我已經幫您寫好了這樣的事件引數:
public class CompletedEventArgs: EventArgs { public CompletedEventArgs(Exception ex) { this.Error = ex; } public Exception Error { get; private set; } }
那麼接下來的工作就交給您了,加油!
嗯?那麼快就寫完啦?再想想?如果真確定了,就展開下面的程式碼對比一下吧:
展開程式碼
隱藏程式碼 public class AsyncTransfer { private Stream m_streamIn; privateStream m_streamOut; public AsyncTransfer(Stream streamIn, Stream streamOut) { this.m_streamIn = streamIn; this.m_streamOut = streamOut; } public void StartAsync() { byte[] buffer = new byte[1024]; this.m_streamIn.BeginRead( buffer, 0, buffer.Length, this.EndReadInputStreamCallback, buffer); } private void EndReadInputStreamCallback(IAsyncResult ar) { var buffer = (byte[])ar.AsyncState; int lengthRead; try { lengthRead = this.m_streamIn.EndRead(ar); } catch (Exception ex) { this.OnCompleted(ex); return; } if (lengthRead <= 0) { this.OnCompleted(null); } else { try { this.m_streamOut.BeginWrite( buffer, 0, lengthRead, this.EndWriteOutputStreamCallback, buffer); } catch (Exception ex) { this.OnCompleted(ex); } } } private void EndWriteOutputStreamCallback(IAsyncResult ar) { try { this.m_streamOut.EndWrite(ar); var buffer = (byte[])ar.AsyncState; this.m_streamIn.BeginRead( buffer, 0, buffer.Length, this.EndReadInputStreamCallback, buffer); } catch (Exception ex) { this.OnCompleted(ex); } } private void OnCompleted(Exception ex) { var handler = this.Completed; if (handler != null) { handler(this, new CompletedEventArgs(ex)); } } public event EventHandler<CompletedEventArgs> Completed; }
是不是很複雜的樣子?
編寫非同步程式,基本則意味著要將原本同步的呼叫拆成兩段:發起及回撥,這樣便讓上下文狀態的儲存便的困難起來。幸運的是,C#這門語言提供了方便好用的匿名函式語法,這對於編寫一個回撥函式來說已經非常容易了。但是,如果需要真正寫一個穩定、安全的非同步程式,需要做的事情還有很多。例如,一次非同步操作結束之後會執行一個回撥函式,那麼如果在這個回撥函式中丟擲了一個異常那該怎麼辦?如果不正確處理這個異常,輕則造成資源洩露,重則造成程序退出。因此在每個回撥函式中,您會發現try...catch塊是必不可少的——甚至還需要兩段。
更復雜的可能還是在於邏輯控制上。這樣一個數據傳輸操作很顯然需要迴圈——讀一段,寫一段。但是由於需要編寫成二段式的非同步呼叫,因此程式的邏輯會被拆得七零八落,我們沒法使用一個while塊包圍整段邏輯。
編寫一個非同步程式本來就是那麼複雜。
編寫簡單的代理
嗯,我們繼續。現在我們已經有了一個非同步傳輸資料的元件,就用它來做一些有趣的事情吧。例如,我們可以在ASP.NET應用程式中建立一個簡單的代理,即給定一個URL,在伺服器端發起這樣一個請求,並將這個URL的資料傳輸到客戶端來。簡單起見,除了進行資料傳輸之外,我們只需要簡單地輸出Content Type頭資訊即可。
寫好了嗎?我也寫了一個,僅供參考:
展開程式碼
隱藏程式碼 public class AsyncWebTransfer { private WebRequest m_request; private WebResponse m_response; private HttpContextBase m_context; private string m_url; public AsyncWebTransfer(HttpContextBase context, string url) { this.m_context = context; this.m_url = url; } public void StartAsync() { this.m_request = WebRequest.Create(this.m_url); this.m_request.BeginGetResponse(this.EndGetResponseCallback, null); } private void EndGetResponseCallback(IAsyncResult ar) { try { this.m_response = this.m_request.EndGetResponse(ar); this.m_context.Response.ContentType = this.m_response.ContentType; var streamIn = this.m_response.GetResponseStream(); var streamOut = this.m_context.Response.OutputStream; var transfer = new AsyncTransfer(streamIn, streamOut); transfer.Completed += (sender, args) => this.OnCompleted(args.Error); transfer.StartAsync(); } catch(Exception ex) { this.OnCompleted(ex); } } private void OnCompleted(Exception ex) { if (this.m_response != null) { this.m_response.Close(); this.m_response = null; } var handler = this.Completed; if (handler != null) { handler(this, new CompletedEventArgs(ex)); } } public event EventHandler<CompletedEventArgs> Completed; }
如果說之前的AsyncTransfer類是基於“Begin/End非同步程式設計模型”實現的基於事件的非同步模式,那麼AsyncWebTransfer便是基於“基於事件的非同步模式”實現的基於事件的非同步模式了。嗯,似乎有點繞口,不過我相信這段程式碼對您來說還是不難理解的。
使用F#完成非同步工作
使用F#實現基於事件的非同步模式
總結
這便是F#的偉大之處。時常有朋友會問我為什麼對F#有那麼大的興趣,我想,如果藉助F#可以用十分之一的時間,十分之一的程式碼行數,寫出執行效果相同,但可維護性高出好幾倍的程式來——我為什麼會不感興趣呢?
您可以在這裡訪問到本文的示例程式碼,其中我在WebApp專案中實現了簡單的入口頁面,您訪問“/image”便會出現兩個文字框,您填入一個URL(例如某幅圖片)並提交,它會將URL提交至“/image/load”或“/image/loadfs”中,它們分別使用了C#和F#實現的AsyncWebTransfer類,從效果上說兩者完全相同。