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

技術譯民發表於2020-08-25

翻譯自 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.Run

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 是你的朋友。

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>>。 為什麼這是相關的? 現在考慮一下,如果我編寫如下的程式碼會發生什麼:

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
譯者 : 技術譯民
出品 : 技術譯站
連結 : 英文原文

相關文章