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