.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
相同的邏輯實現的,只是傳入了一些預設的引數。當你傳遞一個 Action
給 Task.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
提供八個過載,以支援下面的所有組合:
- 無返回值任務(
Task
)和有返回值任務(Task<TResult>
) - 支援取消(
cancelable
)和不支援取消(non-cancelable
) - 同步委託(
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>
,在此例中 TResult
是 Task
,因此 StartNew
的返回值是 Task<Task>
。 類似地,如果我將程式碼改變為:
var t = Task.Factory.StartNew(() =>
{
Task<int> inner = Task.Factory.StartNew(() => 42));
return inner;
});
此時,這裡的 “t” 的型別將會是 Task<Task<int>>
。因為任務委託的型別是 Func<TResult>
,此時 TResult
是 Task<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>>
,TResult
是 Task<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
,很有趣,對吧?