一、线程池
用Thread创建线程时,一定是新线程。如果一直创建新线程来实现逻辑,创建线程将是性能的瓶颈。针对线程的复用就有了线程池的概念,线程池中会始终维持一定数量的线程,当需要新的线程使用时,线程池会分配出空闲的线程,而不需要额外创建。当线程结束后,线程池会回收并等待下次分配。
使用线程池的线程一般使用Task类实现。ThreadPool 中的线程调用算法,其实很简单,每个线程都有一个自己的工作队列local queue,此外线程池中还有一个global queue全局工作队列,首先一个线程被创建出来后,先看看自己的工作队列有没有被分配task,如果没有的话,就去global queue找task,如果还没有的话,就去别的线程的工作队列找Task。
最小线程池个数是CPU
的核心数,最大线程池个数是32767
。dotnet
生成新线程的规则是,当有新任务需要线程时,先在线程池中找空闲线程,如果没有空闲,就等待500ms
看是否有线程空闲出来,如果还是没有,产生一个新线程。
比如4核服务器的线程数就是4
而这点线程对高并发应用来说都不够塞牙缝。虽然 ASP.NET Core runtime 会在线程不够用时自动创建线程,但是每秒只能创建1-2个线程,线程数增加也很慢,导致很多请求被拒绝。
通过将线程池的最小线程数调大去解决问题,但是设置1k以上时,SetMinThreads返回值为false,设置不生效。需用SetMaxThreads先设置最大线程数,之后再SetMinThreads设置最小线程数。更诡异的是SetMaxThreads设置一个比以前小的值,之后再调用SetMinThreads设置最小线程数也是OK的。
设置OK之后程序虽然程序的线程数并不会立马增加,但是一旦遇到高并发请求,线程数增加是非常快速的。差不多1秒几十个线程左右。当并发量降低之后,线程数也会降低。但是当并发再次增高的时候,线程数的增加会比之前更快(一两秒就能恢复到2k水平)
二、异步编程(环境:windows,netcore,控制台程序)
多线程一般是通过异步方法的形式实现的。异步方法用关键字async修饰,通常与await成对使用。调用异步方法时,调用方不会等待,除非加了等待代码。如下面代码中Test()
1.无等待
例如:
不等待时,Test()是否用async修饰成异步方法,对方法内部的执行无影响,结果都为:
即不会等待Task.Run()内的逻辑执行完毕,且内外分属两个线程。
2.使用Wait()或者Task.Result等待
结果如下:
可以看到"主"线程(图中id=1)被阻塞,新线程(图中id=4)结束后,继续使用主线程执行。
2.使用await等待
await只能等待Task或Task<>。await与线程的关系如下:
结果如下:
注意:此时2、3和4使用的是同一个线程(图中id=4)!且主线程(图中id=1)跳过Test方法后续逻辑,继续执行方法外的逻辑到end这一步。
实际上,使用await的情况下,遇到创建新线程时,主线程不会阻塞,而是从创建新线程处开始,主线程跳过await的后续逻辑。新线程执行完await的逻辑后,会继续使用新线程执行异步方法内的后续步骤。连续await或嵌套也是同样的规律。
三、同步方法调用异步方法的问题
参考文章:
总而言之就是,在高并发的场景下,线程池中的线程全被分配去执行全局队列的任务(比如api请求),但是这些请求存在异步方法(本地队列的任务)而被阻塞,本地队列的任务没有分配线程执行。导致CPU使用不高,但线程数一直增加并被阻塞,最终超出SetMaxThreads假死(请求无响应)。
解决思路就是避免阻塞:
1.尽量不要在同步方法调用异步方法,全部用异步方法。即全部使用async/await
2.在能够修改异步方法的情况下
(1) use ConfigureAwait(false) on all of your awaits
(2)Task.Factory.StartNew()
这个方法不使用ThreadPool中的线程
(3)AsyncHelper.RunSync()
Microsoft建立了一个AsyncHelper(内部)类来将Async作为Sync运行
(4)GetAwaiter().GetResult()