最近在测试一个功能代码时发现一个非常奇怪的问题,主要是Task.Run引起一些不符合逻辑的错误,以下针对这一问题排查的总结。
问题代码
可以建个控制台程序来运行以下代码
class Program{static User user = new User();static void Main(string[] args){for (int i = 0; i < 50; i++){Task.Run(user.Init);}System.Threading.Thread.Sleep(-1);}}public class User{private bool mInit = false;private Task OnInit(){Console.WriteLine("User init");System.Threading.Thread.Sleep(1000);return Task.CompletedTask;}public void Init(){lock (typeof(User)){if (!mInit){var task = Task.Run(this.OnInit);if (!task.Wait(5000)){throw new TimeoutException("user init error!");}mInit = true;}}}}
以上代码执行的结果非常奇怪,当在Debug模式下运行,会抛出超时错误。
运行在release模式下则会引起OnIint方法被执行多次,lock完全起不了作用。。
和朋友讨论过程中说lock不要和Task.Run混用,但Task.Wait的实现是基于线程信号量的和async/await是有着本质的差异。抱着解决问题的思路把Task.Run直接改成了线程池方式运行,但结果还是一样。由于找不到问题原因最终去dotnet上提个issues,看一下能提供什么意见。
问题的发现
对于一个程序员来说问题没解决怎能安心呢,隔一天issues没有响应于是开启的解决问题的碰撞模式。在throw timeout里打个断点看一下情况,结果无意中发现Task的状态是WaitingForActivation
状态描述是等待内部调度激活,意思是说这代码并不是不执行或执行有问题,而因为某些状态导致Task还在等待执行中。然后针对这一问题在网查找了一下才发现这问题的原因,主要问题是for 50已经把线和池中的线程抽光了,然然后在Init方法使用Task.Run的时候就只能等待。。。加上方法后面Task.Wait导致当前线程无法回归到池,所以就只能引起超时间异常!如果这里的Task.Wait不加上个超时,那这测试代码就直接处于假死状态无法继续工作,一个等待一个试图获取线程操作从而形成一个类似于死锁的问题!
总结
当你在使用Task.Run时出现一些非常意想不到的结果时可以通过Task.Status状态可以更好的定位到问题。Task默认也是基于线程池的,所以在使用Task.Run和Task.Wait的就要注意这一点,虽然可以通过加大线程池的最小数量来解决低并发问题,但高并发下还是会存在线程资源不足的情况;为了确保不出现类似于死锁的问题,请在使用Task.Wait必须加上超时时间,并且是越短越好,毕竟Wait方法是基于线程阻塞。
BeetleX
开源跨平台通讯框架(支持TLS)
轻松实现高性能:tcp、http、websocket、redis、rpc和网关等服务应用
https://beetlex.io
如果你想了解某方面的知识或文章可以把想法发送到
henryfan@msn.com|admin@beetlex.io