Task.Run(), Task.Factory.StartNew() 和 New Task() 的行為不一致分析

yonlin 發表於 2022-07-01

重現

在 .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 () ...)。讀者可自行嘗試。

這兩個方法行為的差異,可以從原始碼中找到原因:

image

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>,滿足了我們非同步的需求。

參考

Task原始碼:https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs