在學異步,有位園友推薦了《async in C#5.0》,沒找到中文版,恰巧也想提高下英文,用我拙劣的英文翻譯一些重要的部分,純屬娛樂,簡單分享,保持學習,謹記謙虛。
如果你覺得這件事兒沒意義翻譯的又差,盡情的踩吧。如果你覺得值得鼓勵,感謝留下你的贊,願愛技術的園友們在今后每一次應該猛烈突破的時候,不選擇知難而退。在每一次應該獨立思考的時候,不選擇隨波逐流,應該全力以赴的時候,不選擇盡力而為,不辜負每一秒存在的意義。
轉載和爬蟲請注明原文鏈接http://www.cnblogs.com/tdws/p/5628538.html,博客園 蝸牛 2016年6月27日。
在本章,我們將會討論一些關於不使用C#5.0關鍵字async的異步編程。這種方式雖然已經是過去的技術,也許你不會再使用,但這對於你理解異步編程表象背后發生了什么事情是很重要的。也因為這一點,我將會很快的講述示例,僅僅着重揭示出對你理解有幫助的地方。
正如我之前提到的,Silverlight只提供了像web訪問的異步版本API。這里有一個例子,你可以下載一個網頁,並顯示它:
private void DumpWebPage(Uri uri) { WebClient webClient = new WebClient(); webClient.DownloadStringCompleted += OnDownloadStringCompleted; webClient.DownloadStringAsync(uri); } private void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs eventArgs) { m_TextBlock.Text = eventArgs.Result; }
這種API是基於事件的異步模式(EAP)。這個想法是想替代單線程方法去下載網頁,即阻塞型代碼會一直等到下載結束再調用一個方法或觸發一個事件。這個方法看起來和同步代碼一樣,除了無返回類型。這個事件也有一個特別的eventArgs類型,它包含值檢索。
我們在調用這個方法前注冊了事件。該方法立即返回,當然這是因為它是異步代碼。然后在將來的某個時刻觸發。這種模式顯然很復雜,不僅僅是因為你要將它分成像例子一樣的兩個方法。最重要的是,你注冊了一個時間增加了復雜性。如果說我還要用相同的WebClient實例處理其他需求,那么你也許不希望這個時間依然被附加着並且再次執行一遍。
在.NET功能中另一個異步模式設計IAsyncResult接口。其中一個例子就是DNS查找主機名的IP地址,BeginGetHoseAddress。這種設計要求兩個方法,一個是開始執行的BeginMethodName,另一個是執行結束EndMethodName,即你的回調方法。
private void LookupHostName() { object unrelatedObject = "hello"; Dns.BeginGetHostAddresses("oreilly.com", OnHostNameResolved, unrelatedObject); } private void OnHostNameResolved(IAsyncResult ar) { object unrelatedObject = ar.AsyncState; IPAddress[] addresses = Dns.EndGetHostAddresses(ar); // Do something with addresses ... }
至少這種方式不會遭受殘留注冊事件的影響,然而這也額外的對API增加了復雜性。有兩個方法而不是一個,我覺得很不自然。
這兩種異步模式都需要你分為兩個方法來書寫。IAsyncResult模式要你從第一個方法中向第二個方法傳遞某些參數,就像我傳遞了string類型的"hello"。但是這種方式很復雜,即使你不需要這個參數,還是不得不傳遞它,並且迫使你轉換為object類型。
可以說下面這段代碼擁有異步行為,即使不使用async關鍵字,也不用向方法傳遞委托:
void GetHostAddress(string hostName, Action<IPAddress> callback)
我發現這種方式比其他方式更加易用。
private void LookupHostName() { GetHostAddress("oreilly.com", OnHostNameResolved); } private void OnHostNameResolved(IPAddress address) { // Do something with address ... }
不同於兩個方法的模式,像我以前提到的,使用異步方法或者用lambda表達式做回調。它擁有重要的好處就是可以在第一個方法中訪問變量。
private void LookupHostName() { int aUsefulVariable = 3; GetHostAddress("oreilly.com", address => { // Do something with address and aUsefulVariable ... }); }
這個Lambda有一點難以閱讀,並且通常如果你使用多重的異步編程,你將需要很多Lambda表達式相互嵌套,你的代碼將會很快變得犬牙交錯和難以處理。
這種簡單方法的缺點在於他們不再對調用者拋出異常。在之前.NET異步編程中,調用EndMethodName或者得到Result屬性時,將會重新拋出異常,所以在代碼中我們可以相應的處理異常。相反,他們可能在某個錯誤地方停止或者根本不去處理。
任務並行實在.NET Framework4.0版本中推出的。其最重要的地方是Task類,即代表一個正在執行的操作。 泛型版本的Task<T>, 當操作完成時返回類型為T的值。
在C#5.0 async功能上我們大量的使用了Task,我們將會稍后討論。然而即使沒有async,你依然可以使用Task,尤其是使用Task<T>來異步編程。這樣做就行,你開始一個返回Task<T>的操作,然后使用ContinueWith方法注冊你的回掉方法。
private void LookupHostName() { Task<IPAddress[]> ipAddressesPromise = Dns.GetHostAddressesAsync("oreilly.com"); ipAddressesPromise.ContinueWith(_ => { IPAddress[] ipAddresses = ipAddressesPromise.Result; // Do something with address ... }); }
Task的優點就像這個DNS只需要一個方法,使API更加整潔。所有調用異步行為相關的邏輯都可在Task類當中,所以它不需要在每一個方法里都進行復制。這個邏輯可以做很多重要的事兒,比如處理異常和同步上下文(SynchronizationContexts)。這些,我們將會在第八章討論,對於在一個特定線程上執行callback很有用處(比如UI線程)。
最重要的是,Task給我們提供一種使用異步的相對抽象的操作方式。我們可以利用這種組合型去編寫我們的工具,即在很多需要使用Task的情況下提供給一些有用的行為。我們將會看到很多相關的工具組件(utilities)在第七章當中。
正如我們看到的,我們有很多方式來實現異步編程。有一些方式比其他方式整潔易懂易用,但是也希望你已經看出他們共有的缺陷。你打算寫的程序不得不分為兩個方法:實際的方法和回調方法。還有使用異步方法或嵌套多次lambda表達式作為回調,使你的代碼一環套一環難以理解。
實際上這里還有另一個問題。我們已經說過調用一次異步方法的情況,但是當你需要多個異步時會發生什么呢?更糟糕的是,如果弄需要在循環中調用異步又會發生什么呢?你為一個方式是使用遞歸方法,這又比普通的循環難以閱讀多了。
private void LookupHostNames(string[] hostNames) { LookUpHostNamesHelper(hostNames, 0); }
private static void LookUpHostNamesHelper(string[] hostNames, int i) { Task<IPAddress[]> ipAddressesPromise = Dns.GetHostAddressesAsync(hostNames[i]); ipAddressesPromise.ContinueWith(_ => { IPAddress[] ipAddresses = ipAddressesPromise.Result; // Do something with address ... if (i + 1 < hostNames.Length) { LookUpHostNamesHelper(hostNames, i + 1); } }); }
哇!
在這些異步編程方式中,引發的另一個問題就是需要消耗大量代碼。如果你寫一些異步代碼,期望在其他地方使用,你不得不提供API,如果API混亂或者忘記當時的初衷不能理解的話,將會事半功倍。異步代碼是會“傳染”的,因此不僅你需要異步API,還影響調用者和調用者的調用者,知道整個程序亂成一團。
再來談談第二章最后一個示例,我們討論了一個會因從網站下載icons,造成UI線程阻塞,並導致出現應用程序未響應的WPF UI app。現在我們將會看到,將它轉化成手寫的異步代碼。
第首先要做的就是找到一個異步API的版本,我用(WebClient。下載文件)。正如我們已經看到的,WebClient方法使用基於事件的異步方式(EAP),所以我們可以在開始下載之前注冊一個事件作為回調方法。
private void AddAFavicon(string domain) { WebClient webClient = new WebClient(); webClient.DownloadDataCompleted += OnWebClientOnDownloadDataCompleted; webClient.DownloadDataAsync(new Uri("http://" + domain + "/favicon.ico")); } private void OnWebClientOnDownloadDataCompleted(object sender, DownloadDataCompletedEventArgs args) { Image imageControl = MakeImageControl(args.Result); m_WrapPanel.Children.Add(imageControl); }
當然,我們的真正屬於一起的邏輯要被分成兩個方法。我不喜歡使用Lambda來代替剛才的EAP,因為lambda會出現在真正開始下載前,我覺得這是不可讀的。
這個版本的示例也可以在線(https://bitbucket.org/alexdavies74/faviconbrowser)找到,(//譯者注釋:不運行此程序也沒關系,主要是體會下思路就好)在manual分支。如果你運行它,不進界面可相應,圖標也會逐一出現。正因此,我們也引入了一個bug,現在由於所有下載操作同時開始,icons的排序由其下載先后決定,而不是由我的先后請求來決定。如果你想檢驗自己是否理解手動編寫異步代碼,我建議你嘗試着解決此bug。在orderedManual分支下(上面列出的站點下)提供了一個解決方案。其他更有效的解決方案也是有可能的。