Task.Run(), Task.Factory.StartNew() 和 New Task() 的行為不一致分析
重現
在 .Net5 平台下,創建一個控制台程式,注意控制台程式的Main()
方法如下:
static async Task Main(string[] args)
方法的主體非常簡單,使用Task.Run
創建一個立即執行的Task
,在其內部不斷輸出執行緒id,直到手動關閉程式,程式碼如下:
程式碼片段1
點擊查看程式碼
static async Task Main(string[] args)
{
Console.WriteLine("主執行緒執行緒id:" + Thread.CurrentThread.ManagedThreadId);
await Task.Run(async () =>
{
while (true)
{
Console.WriteLine("Fuck World! 執行緒id:" + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000);
Console.WriteLine("執行緒id:" + Thread.CurrentThread.ManagedThreadId);
}
});
}
這段程式碼如期運行,並且不需要在程式末尾使用Console.ReadLine()
控制程式不停止。
但是如果我們使用Task.Factory.StartNew()
替換Task.Run()
的話,程式就會一閃而過,立即退出。
如果使用New Task()
創建的話,如下程式碼所示:
程式碼片段2
點擊查看程式碼
var t = new Task(async () =>
{
while (true)
{
Console.WriteLine("Fuck World!執行緒:" + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000);
Console.WriteLine("執行緒id:" + Thread.CurrentThread.ManagedThreadId);
}
});
t.Start();
await t;
程式依然一閃而過,立即退出。
分析
首先分析下 Task.Run()
和Task.Factory.StartNew()
。
我們將 async
標記的λ表達式當作參數傳入後,編譯器會將λ表達式映射為Func<Task>
或者Func<Task<TResult>>
委託,本示例中因為沒有返回值,所以映射為Func<Task>
。
如果我們用F12
考察 Task.Run()
和Task.Factory.StartNew()
在入參為Func<TResult>
的情況下的返回值類型的話,會發現他們兩者的返回類型都是Task<TResult>
。但是在示例中,你會發現,返回值是不一樣的。Task.Run(async () ...)
的返回類型是Task
,而Task.Factory.StartNew(async () ...)
的返回類型是Task<Task>
。
所以,我們在 await Task.Factory.StartNew(async () ...)
的時候,其實是在await Task<Task>
, 其結果,依然是一個Task
。既然如此,想達到和await Task.Run(async () ...)
的效果就非常簡單了,只需要再加一個await
,即await await Task.Factory.StartNew(async () ...)
。讀者可自行嘗試。
這兩個方法行為的差異,可以從源碼中找到原因:
Task.Run
的內部進行了Unwrap
,把Task<Task>
外層的Task
拆掉了。UnWrap()
方法是存在的,可以直接調用,即Task.Factory.StartNew(async () ...).Unwrap()
,調用後的結果就是Task
。所以await await Task.Factory.StartNew(async () ...)
與await Task.Factory.StartNew(async () ...).Unwrap()
的結果是一致的。在這一點上,Unwrap()
的作用與await
的作用一樣。
也即:await Task.Run(async () ...)
== await await Task.Factory.StartNew(async () ...)
== await Task.Factory.StartNew(async () ...).Unwrap()
。
接下來考察下New Task()
的形式。在程式碼片段2中,雖然調用了await t
,但是程式碼並沒有如期運行,而是一閃而過,程式退出。其實,傳入的參數雖然與之前的一致,但是編譯器並沒有把參數映射為Func<Task>
,而是映射為了Action()
,也就是並沒有返回值。t.Start()
的結果,就是讓那個Action()
開始執行,隨後,Task
執行完畢,await t
也就瞬間完成,沒有任何結果——因為Action()
是沒有返回值的。在這段程式碼當中,Action()
其實運行在一個後台執行緒中,如果在主執行緒上使用Thread.Sleep(10000)
後,會發現控制台一直在輸出內容。
如果想要以New
的方式創建Task
的實例實現同樣的輸出效果,做一下小的改動就可以了,如下所示:
程式碼片段3
點擊查看程式碼
var t = new Task<Task>(async () =>
{
while (true)
{
Console.WriteLine("Fuck World!執行緒:" + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000);
Console.WriteLine("執行緒id:" + Thread.CurrentThread.ManagedThreadId);
}
});
t.Start();
await await t;
將New Task(async ()...)
改為Nwe Task<Task>(async ()...)
就可以了,這樣λ表達式async ()...
就會映射為Func<Task>
,滿足了我們非同步的需求。