关于同步方法里面调用异步方法的探究

关于同步方法里面调用异步方法的探究
前言
我在写代码的时候(.net core)有时候会碰到void方法里,调用async方法并且Wait,而且我还看到别人这么写了。而且我这么写的时候,编译器没有提示任何警告。但是看了dudu的文章:一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相 了解了,这样写是有问题的。但是为什么会有问题呢?我又阅读了dudu文章里提到的一篇博文:.NET Threadpool starvation, and how queuing makes it worse 加上自己亲手实验,写下自己的理解,算是对dudu博文的一个补充和丰富吧。
同步方法里调用异步方法
同步方法里调用异步方法,一种是wait() 一种是不wait()
void fun(){
funAsync.Wait();
funAsync();
}这两种场景都没有编译错误。
首先我们来看一下,在 void里调用 async 方法,并且要等待async的结果出来之后,才能进行后续的操作。
using System;using System.Threading;using System.Threading.Tasks;namespace ConsoleTool2{ class Program
{ static void Main(string[] args)
{
Producer();
} static void Producer()
{ var result = Process().Result; //或者
//Process().Wait();
} static async Task<bool> Process()
{ await Task.Run(() =>
{
Thread.Sleep(1000);
});
Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString()); return true;
}
}
}咱们看这个Producer,这是一个void方法,里面调用了异步方法Process(),其中Process()是一个执行1秒的异步方法,调用的方式是Process().Result 或者Process().Wait()。咱们来运行一遍。
没有任何问题。看起来,这样写完全没有问题啊,不报错,运行也是正常的。
接下来,我们修改一下代码,让代码更加接近生产环境的状态。
using System;using System.Threading;using System.Threading.Tasks;namespace ConsoleTool2{ class Program
{ static void Main(string[] args)
{ while (true)
{
Task.Run(Producer);
Thread.Sleep(200);
}
} static void Producer()
{ var result = Process().Result;
} static async Task<bool> Process()
{ await Task.Run(() =>
{
Thread.Sleep(1000);
});
Console.WriteLine("Ended - " + DateTime.Now.ToLongTimeString()); return true;
}
}
}我们在Main函数里加了for循环,并且1秒钟执行5次Producer(),使用Task.Run(),1秒钟有5个Task产生。相当于生产环境的qps=5。
接下来我们再执行下,看看结果:
在第一秒里只执行了两次Task,就卡住了。我们再看下进程信息:
没有CPU消耗,但是线程数一直增加,直到突破一台电脑的最大线程数,导致服务器宕机。
这明显出现问题了,线程肯定发生了死锁,而且还在不断产生新的线程。
至于为什么只执行了两次Task,我们可以猜测是因为程序中初始的TreadPool 中只有两个线程,所以执行了两次Task,然后就发生了死锁。
现在我们定义一个Produce2() 这是一个正常的方法,异步函数调用异步函数。
static async Task Producer2()
{ await Process();
}我们再Main函数的循环里,执行Producer2() ,执行信息如下:
仔细观察这个图,我们发现第一秒执行了一个Task,第二秒执行了三个Task,从第三秒开始,就稳定执行了4-5次Task,这里的时间统计不是很精确,但是可以肯定从某个时间开始,程序达到了预期效果,TreadPool中的线程每秒中都能稳定的完成任务。而且我们还能观察到,在最开始,程序是反应很慢的,那个时候线程不够用,同时应该在申请新的线程,直到后来线程足够处理这样的情况了。咱们再看看这个时候的进程信息:

线程数一直稳定在25个,也就是说25个线程就能满足这个程序的运行了。
到此我们可以证明,在同步方法里调用异步方法确实是不安全的,尤其在并发量很高的情况下。
探究原因
我们再深层次讨论下为什么同步方法里调用异步方法会卡死,而异步方法调用异步方法则很安全呢?
咱们回到一开始的代码里,我们加上一个初始化线程数量的代码,看看这样是否还是会出现卡死的状况。
由于前面的分析我们知道,这个程序在一秒中并行执行5个Task,每个Task里面也就是Producer 都会执行一个Processer 异步方法,所以粗略估计需要10个线程。于是我们就初始化线程数为10个。
using System;using System.Threading;using System.Threading.Tasks;namespace ConsoleTool2{ class Program
{ static void Main(string[] args)
{
ThreadPool.SetMinThreads(10, 10);
while (true)
{
Task.Run(Producer2);
Thread.Sleep(200);
}
} static void Producer()
{ var result = Process().Result;
} static async Task Producer2()
{ await Process();
} static async Tas关于同步方法里面调用异步方法的探究
声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。如若本站内容侵犯了原著者的合法权益,可联系本站删除。



