.NET Framework 和 .NET Core 之间的线程池差异,线程池饥饿

Posted

技术标签:

【中文标题】.NET Framework 和 .NET Core 之间的线程池差异,线程池饥饿【英文标题】:Thread pool differences between .NET Framework and .NET Core, Thread Pool starvation 【发布时间】:2021-02-20 16:26:07 【问题描述】:

尝试将工作代码从 .Net Framework 4.6.1 传递到 .Net Core 3.1 时,我偶然发现了一个意外行为

这是对代码的简化:

static  void Main(string[] args)

    for (int i = 0; i < 20; i++)
    
        ThreadPool.QueueUserWorkItem(o =>
        
            Console.Write($"In, ");
            RestClient restClient = new RestClient($"http://google.com");
            RestRequest restRequest = new RestRequest();
            var response = restClient.Get(restRequest);

            Console.Write($"Out, ");
        );
    

    Console.ReadLine();

控制台上的预期输出是“In”列表,然后是混合的“In”和“Out”,最后是一些“Out”,这是多线程工作的结果。这在 .Net Framework 上按预期工作。 像这样的:

In, In, In, In, In, In, In, In, In, In, In, In, In, In, In, Out, In, Out,
In, Out, In, Out, In, Out, In, Out, Out, Out, Out, Out, Out, Out, Out,
Out, Out, Out, Out, Out, Out, Out,

但是当在 .Net Core 3.1(同一台机器)上运行完全相同的代码时,看起来我们只有在所有“in”线程完成后才返回写入“out”行(我测试了这个20).

In, In, In, In, In, In, In, In, In, In, In, In, In, In, In, In, In, In,
In, In, Out, Out, Out, Out, Out, Out, Out, Out, Out, Out, Out, Out, Out,
Out, Out, Out, Out, Out, Out, Out,

意味着进程处于饥饿状态,如果添加到线程池的工作项的数量是无限的(例如取决于 API),则永远不会处理 HTTP 响应。

我认为这是因为 ThreadPool 算法选择下一个线程来处理 this is a nice article on the subject 的方式

我不明白为什么它不会在 .Net Framework 上发生,如果我可以让它在 .Net Core 上以某种方式工作。

附:我并不是想避免与 TPL 合作,我只是想弄清楚这一点。

有什么建议吗?

【问题讨论】:

如果您将 REST 调用替换为像 Thread.Sleep(500) 这样的简单阻塞调用,是否也会发生这种情况?我要求排除差异与RestClient 而不是ThreadPool 相关的可能性。 “意味着这个过程中存在饥饿”请解释你是如何得出这个结论的。 @TheodorZoulias 我用睡眠进行了测试,然后它作为例外执行。但是,使用 rest 客户端或常规 httprequest - 在 .net core 8(num cores)上,theads 开始执行,然后池添加 1 个线程,直到 20,并且只有在请求开始完成之后。我不明白为什么会在这种情况下发生这种情况。没有等待,因此与排队的延续无关。 我不太确定这是怎么回事。但是说请求自然需要大约 1 秒。然后我希望启动 8 个线程,然后池每秒左右添加几个新线程,然后请求应该开始完成,因为您的请求是同步的并且它已经在线程池线程上执行 - 它没有'不需要池中的 new 线程来完成请求。当 requset 是异步的并且它需要池中的 new 线程在异步之后执行继续时,就会发生文章中描述的那种饥饿。 @NetanelSwartz 你的问题很有趣。 ThreadPool 的实现可能在 .NET Framework 和 .NET Core 之间发生了变化。我找不到任何关于它的信息。 【参考方案1】:

[已编辑]这是我发现的

.NET Core 和 .NET Framework 的区别在于HttpWebRequest.GetResponse() 的实现。在 .NET Framework 中,它使用 Thread.SpinWait(1),而在 .NET Core 中,它使用 SendRequest().GetAwaiter().GetResult() - 本质上是调用异步实现并对其执行 Wait()。

异步方法调用依赖TaskScheduler 来执行延续。 TaskScheduler 依赖于 ThreadPool。

通常,线程池以 minThreads = # cores 开始。然后它使用某种算法慢慢地增加线程数,直到达到 maxThreads。

代码立即将 20 个阻塞作业发布到线程池。延续作业排在它们之后。线程池会慢慢增加线程数以适应下载作业,然后才会添加一个线程来处理第一个 Continuation 作业。

另一个有趣的转折是,如果您将最小和最大线程都设置为相同的低值并​​运行代码,它就会死锁。那是因为 Continuation 永远不会收到要执行的线程。有关死锁的更多信息here。

有多种方法可以解决这个问题

    避免混合同步和异步代码。一路异步(如果可以的话) 使用ThreadPool.SetMinThreads 以启动足够数量的线程。您至少需要线程数作为预期的并发下载作业数。 在示例代码中,如果您在发布下载作业之间添加 10 到 50 毫秒的延迟,则后续作业有机会在其间安排。

(这个问题使用了一个叫做 RestClient 的东西,它可能在后台使用 HttpClient 或 HttpWebRequest。下面的代码使用 HttpWebRequest)

private static void Main(string[] args)

    //ThreadPool.SetMinThreads(4, 4);
    //ThreadPool.SetMaxThreads(4, 4);
    for (var i = 0; i < 20; i++)
        ThreadPool.QueueUserWorkItem(o =>
        
            Console.Write("In, ");

            var r = (HttpWebRequest) WebRequest.Create("http://google.com");
            r.GetResponse();
            //Try this in .Net Framework and get the same result in as in .NET Core.
            //That's because in .NET Core r.GetResponse() essentially does r.GetResponseAsync().Wait()
            //r.GetResponseAsync().Wait();  

            Console.Write("Out, ");
        );

    Console.ReadLine();

【讨论】:

但是没有GetAsync().Wait(...) 有问题,也没有使用HttpClient。我对HttpWebRequest 进行了同样的尝试(它确实有同步的Get 方法) 原题使用RestClient。不确定它到底是什么,但很可能它在后台使用了 HttpClient。下面是HttpWebRequest.GetResponse的实现:return SendRequest().GetAwaiter().GetResult(); 仍然不知道为什么它在 .NET Framework 和 .NET Core 之间的工作方式不同。 编辑答案以反映更好的发现并回答 .NET FW 和 .NET CORE 之间的区别【参考方案2】:

问题似乎在于 ThreadPool “旨在最大化吞吐量,而不是最小化延迟”。

我认为以下文章为理解这种行为提供了一个很好的起点: Article

【讨论】:

以上是关于.NET Framework 和 .NET Core 之间的线程池差异,线程池饥饿的主要内容,如果未能解决你的问题,请参考以下文章

.NET framework 和.NET有区别?啥区别?

.NET Framework概述

分享 | .NET Framework升级到.NET 5

.NET Framework 和 .NET Core区别总结

net framework怎么更新

.Net Full framework 和 K runtime 使用的 .Net Core Framework 4.5 的区别?