C#中關於Task.Yeild()的探究

      在與同事討論async/await內部實現的時候,忽然想到Task.Yeild()這個函數,爲何呢,瞭解一點C#async/await內部機制的都知道,在await一個異步任務(函數)的時候,它會先判斷該Task是否已經完成,若是已經完成,則繼續執行下去,不會返回到調用方,緣由是儘可能避免線程切換,由於await後面部分的代碼極可能是另外一個不一樣的線程執行,而Task.Yeild()則能夠強制回到調用方,或者說主動讓出執行權,給其餘Task執行的機會,能夠把Task理解爲協程,Task.Yeild()和Thread.sleep(0)有點相同。異步

      爲了證實個人結論成立,請看代碼:async

 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秒鐘,而後回到調用方,打印的時間之差會相隔一秒。函數

image_thumb1_thumb

      Test2()異步函數因爲await了一個未完成的任務(1ms對於CPU來講是很長的了),因此會返回調用方,而後打印相同的時間,一秒鐘以後會打印執行完畢。spa

image_thumb3_thumb

      Test3()調用了Task.Yeild()函數,主動讓出執行權,因此會直接返回調用方,而後打印相同的時間,一秒以後會打印執行完畢。線程

image_thumb5_thumb

      能夠看到,開頭的結論是正確的。那麼,有什麼意義呢?Yeild的意思在這裏其實就是退讓,讓出的意思,讓出什麼呢?就是讓出執行權,這與Thread.sleep(0)讓出CPU執行權給其餘線程(前提是有其餘線程競爭)有機會執行是一個道理。code

      請看個人例子:協程

 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 }

 

image_thumb9_thumb

      能夠看出OP1()和OP2()兩個協程(Task)互相爭用一個線程(用戶模式下的CPU),若是不主動讓出執行權,另外一個協程(Task)將不會有機會執行。blog

      例如:get

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()將永遠不會有機會執行。同步

image20_thumb_thumb

相關文章
相關標籤/搜索