nancy卡顿问题解决记录之不要随便用Task.Run

分析

把nancy的源代码加到项目里一起跑,定位发现nancy host里的监听task并没有跑起来。

task是跑在线程池里的,那没跑起来很可能就是线程池资源不够。

有这个猜想就比较好找问题根源了。

我把代码里大部分task的地方加了控制台输出,发现8个task run起来之后,就会开始卡,后面的task一个一个慢慢启动(差不多每个隔一秒)。

线程与IO和CPU

根据业务需求我们可以将任务分为CPU型和IO型。

CPU主要就是那些需要解析或者计算的任务,会消耗较多的CPU资源。

多线程的运行实际上其实是通过时间片轮转来实现的,保存当前状态,服务下一任务,这种上下文切换是需要时间的。针对CPU密集的多任务,任务越多,花在切换上的时间就越多,效率就越低。所以,想要高效的利用CPU资源,最好的同时进行任务的数量应当等于CPU的核心数。

而IO就主要是通讯,交换数据什么的。IO一般消耗CPU少,任务的大部分时间都在等待IO操作完成。任务在进行IO处理时,CPU就空闲了出来,利用率就不高。这个时候线程适量的多一点会提高CPU效率,让等待的任务空闲,切换成需要CPU计算的任务执行。

像我们现在web应用开发一般都是IO密集型任务,所以线程数量适量多一点会更好。具体适合的数量有一套计算公式,了解一下就行。

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间)*CPU数目

ThreadPool

一般情况下,Task.Run使用的是.NET提供的默认TaskScheduler,它决定何时执行一个异步任务。

  1. 线程池最小线程数默认设置为系统的CPU数

  2. 当达到最小值时,线程池可以创建该类别中的其他线程或等待,直到一些任务完成。

在八核计算机上运行程序时,最小线程数是 8,于是开始的 8 个任务可以立即开始执行。当达到数量 8 而依然没有线程完成执行的时候,线程池会尝试等待任务完成。但是,1 秒后依然没有任务完成,于是线程池创建了一个新的线程来执行新的任务;接下来是每隔一秒会开启一个新的线程来执行现有任务。当有任务完成之后,就可以直接使用之前完成了任务的线程继续完成新的任务。(但因为我们的几乎都是长时间运行的任务,要一直跑着,这个时延就很难顶)

所以对于task,有以下需要注意的点:

  1. 对于 IO 操作,尽量使用原生提供的 Async 方法,这些方法使用的是 IO 完成端口,占用线程池中的 IO 线程而不是普通线程(不要自己使用 Task.Run 占用线程池资源);

  2. 对于没有 Async 版本的 IO 操作,如果可能耗时很长,则指定 CreateOptions 为 LongRunning(这样便会直接开一个新线程,而不是使用线程池)。

  3. 其他短时间执行的任务才推荐使用 Task.Run。

解决

解决我们卡顿的问题的方法通过以上分析已经很显而易见了。我们没有 Async 版本的 IO 操作,且耗时很长,这样就最好不要用线程池,直接新开线程。

也就是把所有的Task.Run换成Task.Factory.StartNew(() => (), TaskCreationOptions.LongRunning)就可以了,以后还是不要乱用Task.Run,唉。

Task.Factory.StartNew(() => serverLoop(), TaskCreationOptions.LongRunning);
//Task.Run(() => serverLoop());

参考

了解 .NET 的默认 TaskScheduler 和线程池(ThreadPool)设置,避免让 Task.Run 的性能急剧降低


本文章使用limfx的vscode插件快速发布