.NET - Task.Run vs Task.Factory.StartNew

翻譯自 Stephen Toub 2011年10月24日的博文《Task.Run vs Task.Factory.StartNew》,Stephen Toub 是微軟並行計算平臺團隊的首席架構師。閉包

.NET 4 中,Task.Factory.StartNew 是安排新任務的首選方法。它有許多重載提供了高度可配置的機制,經過啓用設置選項,能夠傳遞任意狀態、啓用取消,甚至控制調度行爲。全部這些功能的另外一面是複雜性。您須要知道何時使用哪一個重載、提供什麼調度程序等等。另外,Task.Factory.StartNew 用起來並不直截乾脆,至少對於它的一些使用場景來講還不夠快,好比它的主要使用場景——輕鬆地將工做交付到後臺處理線程。架構

所以,在 .NET Framework 4.5 開發者預覽版 中,咱們引入了新的 Task.Run 方法。這決不是要淘汰 Task.Factory.StartNew,而是應該簡單地認爲這是使用 Task.Factory.StartNew 而沒必要傳遞一堆參數的一個便捷方式。這是一個捷徑。事實上,Task.Run 實際是按照與 Task.Factory.StartNew 相同的邏輯實現的,只是傳入了一些默認的參數。當你傳遞一個 ActionTask.Runapp

Task.Run(someAction);

徹底等同於:異步

Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

經過這種方式,Task.Run 就能夠而且應該被用於大多數通用場景——簡單地將工做交給線程池ThreadPool處理(即參數 TaskScheduler.Default 的目標)。這並不意味着 Task.Factory.StartNew 將再也不被使用; 遠非如此,Task.Factory.StartNew 還有不少重要的(當然更高級)用途。你能夠控制 TaskCreationOptions 來控制任務的行爲,能夠控制 TaskScheduler 來控制任務的調度和運行,也可使用接收對象狀態的重載,對於性能敏感的代碼路徑,使用該重載能夠避免閉包和相應的內存分配。不過,對於簡單的狀況,Task.Run 是你的朋友。async

Task.Run 提供八個重載,以支持下面的全部組合:性能

  1. 無返回值任務(Task)和有返回值任務(Task<TResult>)
  2. 支持取消(cancelable)和不支持取消(non-cancelable)
  3. 同步委託(synchronous delegate)和異步委託(asynchronous delegate)

前兩點應該是不言而喻的。對於第一點,有返回 Task 的重載(對於沒有返回值的操做),還有返回 Task<TResult> 的重載(對於返回值類型爲 TResult 的操做)。對於第二點,還有接受 CancellationToken 的重載,若是在任務開始執行以前請求取消,則任務並行庫(TPL)能夠將任務轉換爲取消狀態。線程

第三點是更有趣的,它與 Visual Studio 11 中 C# 和 Visual Basic 的異步語言支持直接相關。 讓咱們暫時考慮一下 Task.Factory.StartNew,這將有助於突出這一區別。若是我編寫下面的調用:翻譯

var t = Task.Factory.StartNew(() =>
{
    Task inner =Task.Factory.StartNew(() => {});
    return inner;
});

這裏的 「t」 的類型將會是 Task<Task>; 由於任務委託的類型是 Func<TResult>,在此例中 TResultTask,所以 StartNew 的返回值是 Task<Task>。 相似地,若是我將代碼改變爲:代理

var t = Task.Factory.StartNew(() => 
{ 
    Task<int> inner = Task.Factory.StartNew(() => 42)); 
    return inner; 
});

此時,這裏的 「t」 的類型將會是 Task<Task<int>>。由於任務委託的類型是 Func<TResult>,此時 TResultTask<int>,所以 StartNew 的返回值是 Task<Task<int>>。 爲何這是相關的? 如今考慮一下,若是我編寫以下的代碼會發生什麼:code

var t = Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
});

這裏經過使用 async 關鍵詞,編譯器會將這個委託(delegate)映射成 Func<Task<int>>,調用該委託會返回 Task<int> 表示此調用的最終完成。由於委託是 Func<Task<int>>TResultTask<int>,所以這裏 「t」 的類型將會是 Task<Task<int>>,而不是 Task<int>

爲了處理這類狀況,在 .NET 4 咱們引入了 Unwrap 方法。Unwrap 方法有兩個重載,都是擴展方法,一個針對類型 Task<Task>,一個針對類型 Task<Task<TResult>>。咱們稱此方法爲 Unwrap,由於實際上它「解包」了內部任務,將內部任務的返回值做爲了外部任務的返回值而返回。對 Task<Task> 調用 Unwrap 返回一個新的 Task(咱們一般將其稱爲代理),它表示該內部任務的最終完成。相似地,對 Task<Task<TResult>> 調用 Unwrap 返回一個新的 Task<TResult> 表示該內部任務的最終完成。(在這兩種狀況下,若是外部任務出錯或被取消,則不存在內部任務,由於沒有運行到完成的任務不會產生結果,所以代理任務表示外部任務的狀態。) 回到前面的例子,若是我但願 「t」 表示那個內部任務的返回值(在此例中,值是 42),我能夠編寫:

var t = Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
}).Unwrap();

如今,這裏 「t」 變量的類型將會是 Task<int>,表示異步調用的返回值。

回到 Task.Run。由於咱們但願人們將工做轉移到線程池(ThreadPool)中並使用 async/await 成爲廣泛現象,因此咱們決定將此解包(unwrapping)功能構建到 Task.Run 中。這就是上面第三點中提到的內容。有多種 Task.Run 的重載,它們接受 Action(針對無返回值任務)、 Func<TResult>(針對返回 TResult 的任務)、Func<Task>(針對無返回值的異步任務) 和 Func<Task<TResult>>(針對返回 TResult 的異步任務)。在內部,Task.Run 會執行與上面 Task.Factory.StartNew 所示的一樣類型的解包(unwrapping)操做。因此,當我寫下:

var t = Task.Run(async delegate
{
    await Task.Delay(1000);
    return 42;
});

「t」 的類型是 Task<int>Task.Run 的這種重載實現基本上等效於:

var t = Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000); 
    return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();

如前所述,這是一條捷徑。

全部這些都意味着您能夠將 Task.Run 與常規lambdas/匿名方法或與異步lambdas/匿名方法一塊兒使用,都會發生正確的事情。若是我想將工做交給線程池(ThreadPool)並等待其結果,例如:

int result = await Task.Run(async () =>
{
    await Task.Delay(1000);
    return 42;
});

變量 result 的類型將會是 int,正如您指望的那樣,在調用此任務大約一秒種後,變量 result 的值將被設置爲 42。

有趣的是,幾乎能夠將新的 await 關鍵字看做是與 Unwrap 方法等效的語言。所以,若是咱們返回到 Task.Factory.StartNew 示例,則可使用 Unwrap 重寫上面最後一個代碼片段,以下:

int result = await Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default).Unwrap();

或者,我可使用第二個 await 來代替使用 Unwrap

int result = await await Task.Factory.StartNew(async delegate
{
    await Task.Delay(1000);
    return 42;
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

這裏的 「await await」 不是輸入錯誤,Task.Factory.StartNew 返回 Task<Task<int>>await Task<Task<int>> 返回 Task<int>,而後 await Task<int> 返回 int,頗有趣,對吧?


做者 : Stephen Toub
譯者 : 技術譯民
出品 : 技術譯站
連接 : 英文原文

相關文章
相關標籤/搜索