在與同事討論async/await內部實現的時候,突然想到Task.Yeild()這個函數,為什麼呢,瞭解一點C#async/await內部機制的都知道,在await一個非同步任務(函數)的時候,它會先判斷該Task是否已經完成,如果已經完成,則繼續執行下去,不會返回到調用方,原因是儘量避免線程切換, ...
在與同事討論async/await內部實現的時候,突然想到Task.Yeild()這個函數,為什麼呢,瞭解一點C#async/await內部機制的都知道,在await一個非同步任務(函數)的時候,它會先判斷該Task是否已經完成,如果已經完成,則繼續執行下去,不會返回到調用方,原因是儘量避免線程切換,因為await後面部分的代碼很可能是另一個不同的線程執行,而Task.Yeild()則可以強制回到調用方,或者說主動讓出執行權,給其他Task執行的機會,可以把Task理解為協程,Task.Yeild()和Thread.sleep(0)有點相同。
為了證明我的結論成立,請看代碼:
1 public static async Task Test1() 2 { 3 await Task.CompletedTask; 4 Thread.Sleep(1000); 5 Console.WriteLine("Test1任務完成"); 6 } 7 public static async Task Test2() 8 { 9 await Task.Delay(1); 10 Thread.Sleep(1000); 11 Console.WriteLine("Test2任務完成"); 12 } 13 public static async Task Test3() 14 { 15 await Task.Yield(); 16 Thread.Sleep(1000); 17 Console.WriteLine("Test3任務完成"); 18 } 19 static void Main(string[] args) 20 { 21 Console.WriteLine(DateTime.Now); 22 _ = Test1(); 23 Console.WriteLine(DateTime.Now); 24 Console.ReadLine(); 25 }
按照開頭的理論,Test1()非同步函數由於await了一個已經完成的任務,所以會繼續往下執行,阻塞1秒鐘,然後回到調用方,列印的時間之差會相隔一秒。
Test2()非同步函數由於await了一個未完成的任務(1ms對於CPU來說是很長的了),所以會返回調用方,然後列印相同的時間,一秒鐘之後會列印執行完畢。
Test3()調用了Task.Yeild()函數,主動讓出執行權,所以會直接返回調用方,然後列印相同的時間,一秒之後會列印執行完畢。
可以看到,開頭的結論是正確的。那麼,有什麼意義呢?Yeild的意思在這裡其實就是退讓,讓出的意思,讓出什麼呢?就是讓出執行權,這與Thread.sleep(0)讓出CPU執行權給其他線程(前提是有其他線程競爭)有機會執行是一個道理。
請看我的例子:
1 public static async Task OP1() 2 { 3 while (true) 4 { 5 await Task.Yield();//這裡會捕捉同步上下文,由於是控制台程式,沒有同步上下文,所以預設的線程池任務調度器變成同步上下文 6 //也就是說後面的代碼將會線上程池上執行,由於線程池工作線程數量設置為1,所以必須主動讓出執行權,讓其他的 7 //任務有執行的機會 8 Console.WriteLine("OP1在執行"); 9 Thread.Sleep(1000);//模擬一些需要占用CPU的操作 10 } 11 } 12 public static async Task OP2() 13 { 14 while (true) 15 { 16 await Task.Yield(); 17 Console.WriteLine("OP2在執行"); 18 Thread.Sleep(1000); 19 } 20 } 21 static async Task Main(string[] args) 22 { 23 ThreadPool.SetMinThreads(1, 1); 24 ThreadPool.SetMaxThreads(1, 1); 25 //Task.Run()方法預設使用線程池任務調度器執行任務,由於主線程不是線程池線程,所以使用Task.Run() 26 var t = Task.Run(async () => 27 { 28 var t1 = OP1(); 29 var t2 = OP2(); 30 await Task.WhenAll(t1, t2); 31 }); 32 await t; 33 Console.ReadLine(); 34 }
可以看出OP1()和OP2()兩個協程(Task)互相爭用一個線程(用戶模式下的CPU),如果不主動讓出執行權,另一個協程(Task)將不會有機會執行。
例如:
1 public static async Task OP2() 2 { 3 while (true) 4 { 5 await Task.CompletedTask;//或者是直接去掉 6 Console.WriteLine($"OP2在執行 {DateTime.Now}"); 7 Thread.Sleep(1000); 8 } 9 }
這樣OP1()將永遠不會有機會執行。