異步編程系列第03章 自己寫異步代碼


寫在前面

  在學異步,有位園友推薦了《async in C#5.0》,沒找到中文版,恰巧也想提高下英文,用我拙劣的英文翻譯一些重要的部分,純屬娛樂,簡單分享,保持學習,謹記謙虛。

  如果你覺得這件事兒沒意義翻譯的又差,盡情的踩吧。如果你覺得值得鼓勵,感謝留下你的贊,願愛技術的園友們在今后每一次應該猛烈突破的時候,不選擇知難而退。在每一次應該獨立思考的時候,不選擇隨波逐流,應該全力以赴的時候,不選擇盡力而為,不辜負每一秒存在的意義。

   轉載和爬蟲請注明原文鏈接http://www.cnblogs.com/tdws/p/5628538.html,博客園 蝸牛 2016年6月27日。

目錄

第01章 異步編程介紹

第02章 為什么使用異步編程

第03章 手動編寫異步代碼

    .NET中的一些異步模式
    最簡單的異步模式
    關於Task的介紹
    手動編寫異步代碼的問題
    使用手寫異步代碼轉換示例(第二章最后一個示例)

第04章 編寫Async方法

第05章 Await究竟做了什么

第06章 以Task為基礎的異步模式

第07章 異步代碼的一些工具

第08章 哪個線程在運行你的代碼

第09章 異步編程中的異常

第10章 並行使用異步編程

第11章 單元測試你的異步代碼

第12章 ASP.NET應用中的異步編程

第13章 WinRT應用中的異步編程

第14章 編譯器在底層為你的異步做了什么

第15章 異步代碼的性能

手動編寫異步代碼

  在本章,我們將會討論一些關於不使用C#5.0關鍵字async的異步編程。這種方式雖然已經是過去的技術,也許你不會再使用,但這對於你理解異步編程表象背后發生了什么事情是很重要的。也因為這一點,我將會很快的講述示例,僅僅着重揭示出對你理解有幫助的地方。

 
.NET中的一些異步模式

  正如我之前提到的,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屬性時,將會重新拋出異常,所以在代碼中我們可以相應的處理異常。相反,他們可能在某個錯誤地方停止或者根本不去處理。

 
關於Task的介紹

  任務並行實在.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分支下(上面列出的站點下)提供了一個解決方案。其他更有效的解決方案也是有可能的。

寫在后面
27號入職,花了三天的業余時間,坎坎坷坷的翻譯了第三章。如果你對你有些許益處,不要吝嗇你的贊,給個鼓勵。不准確和需要補充的地方,也請前輩們不吝賜教,我將虛心改正。下一章將會介紹 “ 編寫Async方法


免責聲明!

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



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