首页 > 解决方案 > .NET Framework 和 .NET Core 之间的线程池差异,线程池饥饿

问题描述

尝试将工作代码从 .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算法选择下一个线程来处理的方式是关于这个主题的一篇不错的文章

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

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

有什么建议么?

标签: c#multithreadingthreadpool

解决方案


[已编辑]这是我发现的

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

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

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

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

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

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

  1. 避免混合同步和异步代码。一路异步(如果可以的话)
  2. 用于ThreadPool.SetMinThreads从足够数量的线程开始。您至少需要线程数作为预期的并发下载作业数。
  3. 在示例代码中,即使在发布下载作业之间添加 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();
}

推荐阅读