C#中关于Task.Yeild()的探究
在与同事讨论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()将永远不会有机会执行。
作者: 白烟染黑墨
出处:https://www.cnblogs.com/hkfyf/p/13276411.html
版权:本文采用「署名-非商业性使用-相同方式共享 4.0 国际」知识共享许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端