一、线程池

用Thread创建线程时,一定是新线程。如果一直创建新线程来实现逻辑,创建线程将是性能的瓶颈。针对线程的复用就有了线程池的概念,线程池中会始终维持一定数量的线程,当需要新的线程使用时,线程池会分配出空闲的线程,而不需要额外创建。当线程结束后,线程池会回收并等待下次分配。

        使用线程池的线程一般使用Task类实现。ThreadPool 中的线程调用算法,其实很简单,每个线程都有一个自己的工作队列local queue,此外线程池中还有一个global queue全局工作队列,首先一个线程被创建出来后,先看看自己的工作队列有没有被分配task,如果没有的话,就去global queue找task,如果还没有的话,就去别的线程的工作队列找Task。

        最小线程池个数是CPU的核心数,最大线程池个数是32767dotnet 生成新线程的规则是,当有新任务需要线程时,先在线程池中找空闲线程,如果没有空闲,就等待500ms看是否有线程空闲出来,如果还是没有,产生一个新线程。

比如4核服务器的线程数就是4

ThreadPool.ThreadCount: 4, Minimum work threads: 4, Minimum completion port threads: 4

        而这点线程对高并发应用来说都不够塞牙缝。虽然 ASP.NET Core runtime 会在线程不够用时自动创建线程,但是每秒只能创建1-2个线程,线程数增加也很慢,导致很多请求被拒绝。

        通过将线程池的最小线程数调大去解决问题,但是设置1k以上时,SetMinThreads返回值为false,设置不生效。需用SetMaxThreads先设置最大线程数,之后再SetMinThreads设置最小线程数。更诡异的是SetMaxThreads设置一个比以前小的值,之后再调用SetMinThreads设置最小线程数也是OK的。

        设置OK之后程序虽然程序的线程数并不会立马增加,但是一旦遇到高并发请求,线程数增加是非常快速的。差不多1秒几十个线程左右。当并发量降低之后,线程数也会降低。但是当并发再次增高的时候,线程数的增加会比之前更快(一两秒就能恢复到2k水平)

二、异步编程(环境:windows,netcore,控制台程序)

多线程一般是通过异步方法的形式实现的。异步方法用关键字async修饰,通常与await成对使用。调用异步方法时,调用方不会等待,除非加了等待代码。如下面代码中Test()

1.无等待

例如:

static async void Test()
{
            Console.WriteLine("1:" + Thread.CurrentThread.ManagedThreadId);
             Task.Run(() =>
            {
                Console.WriteLine("2:" + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(5000);
                Console.WriteLine("3:" + Thread.CurrentThread.ManagedThreadId);
            });
            Console.WriteLine("4:" + Thread.CurrentThread.ManagedThreadId);
}

不等待时,Test()是否用async修饰成异步方法,对方法内部的执行无影响,结果都为:

 即不会等待Task.Run()内的逻辑执行完毕,且内外分属两个线程。

2.使用Wait()或者Task.Result等待

static async void TestAsync()
{
            Console.WriteLine("1:" + Thread.CurrentThread.ManagedThreadId);
            Task.Run(() =>
            {
                Console.WriteLine("2:" + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(5000);
                Console.WriteLine("3:" + Thread.CurrentThread.ManagedThreadId);
            }).Wait();
            Console.WriteLine("4:" + Thread.CurrentThread.ManagedThreadId);
}

结果如下:

 可以看到"主"线程(图中id=1)被阻塞,新线程(图中id=4)结束后,继续使用主线程执行。

2.使用await等待

        await只能等待Task或Task<>。await与线程的关系如下:

static async void TestAsync()
{
            Console.WriteLine("1:" + Thread.CurrentThread.ManagedThreadId);
            await Task.Run(() =>
            {
                Console.WriteLine("2:" + Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(5000);
                Console.WriteLine("3:" + Thread.CurrentThread.ManagedThreadId);
            });
            Console.WriteLine("4:" + Thread.CurrentThread.ManagedThreadId);
}

结果如下:

        注意:此时2、3和4使用的是同一个线程(图中id=4)!且主线程(图中id=1)跳过Test方法后续逻辑,继续执行方法外的逻辑到end这一步。

        实际上,使用await的情况下,遇到创建新线程时,主线程不会阻塞,而是从创建新线程处开始,主线程跳过await的后续逻辑。新线程执行完await的逻辑后,会继续使用新线程执行异步方法内的后续步骤。连续await或嵌套也是同样的规律。

三、同步方法调用异步方法的问题

参考文章:一码阻塞,万码等待:ASP.NET Core 同步方法调用异步方法“死锁”的真相 .NET Threadpool starvation, and how queuing makes it worse 

        总而言之就是,在高并发的场景下,线程池中的线程全被分配去执行全局队列的任务(比如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()