異步編程系列06章 以Task爲基礎的異步模式(TAP)

寫在前面

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

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

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

目錄

第01章 異步編程介紹緩存

第02章 爲何使用異步編程網絡

第03章 手動編寫異步代碼app

第04章 編寫Async方法框架

第05章 Await究竟作了什麼異步

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

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

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

第09章 異步編程中的異常

第10章 並行使用異步編程

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

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

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

第14章 編譯器在底層爲你的異步作了什麼

第15章 異步代碼的性能

以Task爲基礎的異步模式

  基於Task的異步編程模式(TAP)是Microsoft爲.Net平臺下使用Task進行編程所提供的一組建議和文檔—地址(譯者:後續也會翻譯此文檔,寫的確實不錯):http://www.microsoft.com/en-gb/download/details.aspx?id=19957。微軟並行編程團隊的Stephen Toub在文檔中提供了好的例子,很值得一讀。

  這種模式提供了能夠被await消耗(調用)方法的APIs,而且當使用async關鍵字編寫遵照這種模式的方法時,手寫Task一般頗有用。在本章,我將介紹如何使用這種模式和技術。

TAP具體指什麼?

  我假設咱們已經知道如何使用C#設計一個好的異步方法:

  ·它應該有儘可能少的參數,甚至不要參數。若是可能的話必定要避免ref和out參數。

  ·若是有意義的話,他應該有一個返回類型,他能真正的表達方法代碼的結果,而不是像C++那種成功標識。

  ·它應該有一個能夠解釋本身行爲的命名,而不依賴於額外的符號或註釋。

  預期內的錯誤應該做爲返回類型的一部分,而非預期內的則應拋出異常。

  這裏有一個DNS類下的,設計的不錯的異步方法:

public static IPHostEntry GetHostEntry(string hostNameOrAddress)

  TAP提供了設計異步方法相同的準則,基於你已經掌握的異步方法技能。以下:

  ·他應該和異步方法有相同的參數,ref和out參數必定要避免。

  ·他應該返回Task或者Task<T>,固然這取決你你的方法是否有返回類型。這個任務應該在未來的某個時刻完成,並提供結果。

  ·他應該被命名爲NameAsync,Name等價於你表示做用的異步方法名字。

  ·因爲方法運行中咱們錯誤(預期外)的致使的異常應該被直接拋出。任何其餘異常(預期內)應該由Task來帶出。

  下面是一個好的TAP方法設計:

public static Task<IPHostEntry> GetHostEntryAsync(string hostNameOrAddress)

  這一切彷佛很是明顯,可是像咱們在以前講過的.NET異步編程的幾種模式http://www.cnblogs.com/tdws/p/5628538.html#one,這是第三種正是在.NET框架下應用的異步模式,而且我肯定有無數的非正式的方式來寫異步代碼。

  TAP的關鍵理念是異步方法返回Task,即封裝了未來完成耗時操做的結果。若是沒有這個理念,咱們過去的 異步模式不是要給方法增長額外參數,就是要增長額外方法或者事件來支撐回調機制。Task能夠包含任何回調所須要的基礎內容,而不須要以往雜亂的細節來污染你的方法,形成閱讀和書寫困難。

  額外的好處是,因爲異步回調的機制如今在Task中,在異步調用時你不須要處處複製和準備回調方法。反過來這意味着這種機制可以承擔更加複雜強大的任務,使其可以作一些可行的事兒像恢復上下文,包括同步上下文。它也提供了一個通用的API用於處理異步操做,使編譯器功能像async同樣合理,其餘模式則達不到這種效果。

使用Task來作計算密集型的操做

  有時,一個耗時操做既不作網絡請求也不訪問磁盤;他只是在一個須要不少處理器時間的複雜運算上耗費了時間。固然,咱們不能期望作到這一點像網絡請求同樣不佔用線程。可是在程序的UI上,咱們依然但願避免UI凍結形成不響應。爲了解決這件事兒,咱們不得不返回UI線程來處理其餘事件,而且用一個不一樣的線程來作耗時計算。

  Task提供了一種很簡單的方法來作這件事,而且你可使用await像其餘異步同樣,從而在計算完成是更新UI界面:(譯者註釋:Task.Run()能夠開啓一個新的後臺線程。)

Task t = Task.Run(() => MyLongComputation(a, b));

  Task.Run方法使用了ThreadPool中的一個線程來執行你給的委託。在這種狀況下,我使用了一個lambda使其更加容易傳遞本地變量到計算當中。由此產生的Task當即開始,而且咱們能夠await這個Task:

await Task.Run(() => MyLongComputation(a, b));

  這是一個在後臺線程工做的很簡單的方式。

  好比你須要更多的控制,好比使用哪一個線程執行計算或者如何排隊。Task有一個靜態的叫作TaskFactory類型的Factory的屬性。它有一個StratNew方法能夠控制你計算的執行:

Task t = Task.Factory.StartNew(() => MyLongComputation(a, b),
cancellationToken,
TaskCreationOptions.LongRunning,
taskScheduler);

  若是你在編寫一個包含大量計算密集型的方法的類庫,你也許h忽視了去給你的方法提供一個異步版本,便可以經過調用Task.Run來開始工做在後臺線程中的版本。這不是一個好主意,由於你的API調用者比你更瞭解應用程序的線程需求。舉個例子。在web應用中,使用線程池沒有好處;惟一應該優化的是線程總數。Task.Run是一個很簡單的調用,因此若是須要的話就給調用者留下API以來調用吧。

建立一個可控的Task

  TAP真的很容易被消費(調用),因此你能夠在你全部的接口中很容易的提供TAP模式。咱們已經知道在消費其餘TAP API時如何作,也知道如何使用使用異步方法。可是當耗時操做沒有可用的TAP API會怎樣呢?也許這是一個使用其餘異步模式的API,也許你沒有在消費(調用)一個API而是在作一些徹底手動的異步。

  這裏咱們使用的工具是TaskCompletionSource<T>這是一種受你控制建立Task的方式。你可使Task在任何你想要的時候完成,你也能夠在任何地方給它一個異常讓它失敗。

  咱們來看看這個例子。假設你想經過下面這個方法封裝一個提示展現給用戶:

Task<bool> GetUserPermission()

  這封裝的是一個提示用戶是否贊成的自定義對話框。由於用戶的許可在你程序裏不少的地方須要,定義一個容易調用的方法是很重要的。這是一個很棒的地方來使用異步方法,由於你必定想釋放UI線程來實現展現對話框。可是它也不接近傳統的用於網絡請求和其餘耗時操做的異步方法。在這裏,咱們是等待用戶,咱們來看看這個方法的內部。

private Task<bool> GetUserPermission()
{
    // Make a TaskCompletionSource so we can return a puppet Task
    TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
    // Create the dialog ready
    PermissionDialog dialog = new PermissionDialog();
    // When the user is finished with the dialog, complete the Task using SetResult
    dialog.Closed += delegate { tcs.SetResult(dialog.PermissionGranted); };
    // Show the dialog
    dialog.Show();
    // Return the puppet Task, which isn't completed yet
            return tcs.Task;
}

  注意這個方法沒有被標記爲async;咱們手動的建立了一個Task,因此咱們不但願編譯器爲咱們建立一個。TaskCompletionSource<T>建立了這個Task,而且將它做爲一個屬性來返回,咱們以後可使用SetResult方法在TaskCompletionSource上使得該Task完成。

  因爲咱們遵照了TAP,咱們的調用者就可使用await來等待用戶的許可,這個調用很天然。

if (await GetUserPermission())
{ ...

  有一個煩惱就是TaskCompletionSource<T>沒有一個非泛型版本。然而因爲Task<T>是Task的父類,你能夠在任意你須要Task的地方使用Task<T>。反過來意味着你可使用一個TaskCompletionSource<T>,而且由一個Task做爲屬性所返回的Task<T>是徹底有效的。我每每使用一個TaskCompletionSource<Object>而且調用SetResult(null)來完成它。你能夠很容易個建立一個非泛型TaskCompletionSource若是你須要的話,能夠基於一個泛型的(譯者:把泛型的做爲父類)。

與舊的異步模式相互做用

  .NET團隊在框架的全部重要的異步編程API上建立了TAP模式的版本。但頗有趣的是去理解如何把一個非TAP模式的異步代碼構建成TAP的,在這樣的狀況下,你須要和已有的異步代碼相互做用。下面有一個如何使用TaskCompletionSource<T>的有趣的例子。

  咱們來檢查一下以前使用DNS例子的方法。在.NET4.0中,這個DNS方法使用的異步版本是IAsyncResult模式。這意味着它有Begin方法和End方法組成:

IAsyncResult BeginGetHostEntry(string hostNameOrAddress,
AsyncCallback requestCallback,
object stateObject)
IPHostEntry EndGetHostEntry(IAsyncResult asyncResult)

  一般狀況,你也許消費(調用)這個API經過使用一個lambda做爲回調,而且在其中咱們要調用End方法。咱們要在此作的很明確,值使用一個TaskCompletionSource<T>去完成一個Task。

public static Task<IPHostEntry> GetHostEntryAsync(string hostNameOrAddress)
{
    TaskCompletionSource<IPHostEntry> tcs = new TaskCompletionSource<IPHostEntry>();
    Dns.BeginGetHostEntry(hostNameOrAddress, asyncResult =>
    {
        try
        {
        IPHostEntry result = Dns.EndGetHostEntry(asyncResult);
        tcs.SetResult(result);
        }
        catch (Exception e)
        {
        tcs.SetException(e);
        }
    }, null);
    return tcs.Task;
}

  這段代碼被可能的異常變得更復雜。若是該DNS方法失敗,在咱們調用EndGetHostEntry方法時會拋出異常。這就是爲何IAsyncResult模式在End方法中使用了一個比僅僅傳遞結果到回調方法中更使人費解的系統。當異常被拋出時,咱們應該把它裝載到咱們的TaskCompletionSource<T>當中,以便咱們的調用者能夠經過TAP的形式拿到異常。

  實際上,咱們有足夠的API遵循這種模式,像.NET框架團隊編寫了一個工具方法,能夠將它們轉換成TAP版本,好比下面這樣:

Task t =  Task<IPHostEntry>.Factory.FromAsync<string>(Dns.BeginGetHostEntry,
                                  Dns.EndGetHostEntry,
                                  hostNameOrAddress,
                                  null);

  它將Begin和End方法做爲委託,而且使用的機制和咱們之前的很像。但它可能比咱們簡單的方法更高效。

冷Task和熱Task

  在.NET4.0中,任務並行庫最初提成了Task類型,他有一個 cold Task的概念,這還是須要開始的,與其相對的hot Task,則是已經在運行的。目前爲止,咱們僅處理了hot Task。

  TAP明確指出全部Task在從方法返回前必須是hot的。幸運的是,咱們以前所講的全部技術中建立的Task都是hot Task。例外的是TaskCompletionSource<T>,它實際上沒有什麼cold和hot Task的概念。只須要確保在將來某個時刻完成可該Task。

前期工做

  咱們已經知道當你調用一個TAP的異步方法,和其餘任何方法同樣,這個方法在當前線程中運行。不一樣點在於TAP方法在返回前沒有真正的完成工做。他會當即返回一個Task,而且Task將會在實際工做結束後完成。

  咱們已經說過,一些代碼將會在方法中同步的運行,而且在當前線程。在這種狀況下的異步方法,至少代碼可達,而且包括操做數,第一個await,正如咱們在「異步方法直到被須要前是同步的」所講到的。

  TAP建議經過TAP方法所作的同步工做應儘量最少數量。你能夠檢查參數是否有效,也能夠經過掃描一個緩存來避免耗時操做,而且你也不該該在其中作一個緩慢的計算。混合的方法,即作一些運算,接着作一些網絡請求或相似的事情是很好的辦法,可是你應該使用Task.Run將該計算移到後臺線程。想象一下常規的功能上傳圖片到網站上,但須要首先調整大小來節省帶寬:

Image resized = await Task.Run(() => ResizeImage(originalImage));
await UploadImage(resized);

  這在UI app上是很重要的,這對於web app沒有什麼特別的好處。當咱們看見一個遵照TAP模式的方法,咱們但願他迅速返回。任何使用你代碼的人,並將其移動到UI app中,若是你的圖片調整很是緩慢,將會是一個「驚喜」

寫在最後

  這周更新的有點慢。須要英文原著的能夠私信或留言。若是有錯誤,但願能指出。下一篇將介紹:異步代碼的一些工具方法

相關文章
相關標籤/搜索