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

分類:IT技術 時間:2016-10-11
寫在前面

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

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

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

目錄

第01章 異步編程介紹

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

第03章 手動編寫異步代碼

第04章 編寫Async方法

第05章 Await究竟做了什麽

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

第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中,如果你的圖片調整非常緩慢,將會是一個“驚喜”

寫在最後

  這周更新的有點慢。需要英文原著的可以私信或留言。如果有錯誤,希望能指出。下一篇將介紹:異步代碼的一些工具方法


Tags: 英文翻譯 Microsoft Stephen 編譯器 中文版

文章來源:


ads
ads

相關文章
ads

相關文章

ad