减少 .net 控制台应用程序中的线程数

Reduce number of threads in .net console application

提问人:Sierrodc 提问时间:10/31/2023 最后编辑:Sierrodc 更新时间:11/7/2023 访问量:63

问:

问题

我有一个.Net7 控制台应用程序,在多核 Linux 服务器上并行执行(同一程序的许多实例)。 显然,服务器有一个限制 (ulimit -a),它限制了每个用户创建的线程数。如果应用程序想要创建更多线程,则会引发 OutOfMemory 异常 (https://github.com/dotnet/runtime/issues/71761)。

如何重现问题(在 linux/wsl 上):

for (var i = 0; i < int.Parse(args[0]); i++)
{
    var newThread = new Thread(() => Thread.Sleep(10_000));
    newThread.Start();
    if((i+1)%1000 == 0) Console.WriteLine($"Threads: {i+1}");
}
Console.WriteLine("ctrl+c to kill");

并运行

ulimit -u 4096

这会将限制设置为 4096。然后运行

dotnet run 5000

dotnet run 2500 & dotnet run 2500 &

该代码将尝试在单个进程中创建 5000 个线程或在两个进程中创建 5000 个线程,但系统在这两种情况下都会引发 4096 作为 limit => OutOfMemory。

考虑运行 100 个程序实例...如果程序尝试生成 40 个线程,则会引发 OutOfMemory 异常。

德西德拉塔

我们正在尝试增加这样的限制,但我想知道为什么会生成这么多线程(让我们使用 #ObsTh = )。 信息:Process.GetCurrentProcess().Threads.Length

  • 这是一个.Net7 控制台应用程序,在发布模式下发布,linux-x64,独立
  • 该应用程序本身不会启动线程,它只是使用 async/await 范式。顶级语句中的第一行代码:#ObsTh=10 个线程
  • 应用执行少量 HttpClient.GetAsync() 请求。从 中注册的 中检索。#ObsTh=22 个线程 (+12)HttpClientIHttpClientFactoryServiceCollection
  • 该应用程序使用 .连接后,#ObsTh=专用线程池中的 32 个线程 (+10)。StackExchange.Redis
  • 有时线程数达到 35-40 个线程,我的应用程序“基本上”是一个单线程应用程序。

现在。。。排除(但如果你知道一些事情,请告诉我),如何减少控制台应用中的线程数?StackExchange.Redis


只有几行代码显示了该行为(我不包括redis):

using System.Diagnostics;
ThreadPool.SetMaxThreads(1, 1);

Console.WriteLine($"Start process: {Process.GetCurrentProcess().Threads.Count}");

HttpClient c = new HttpClient();
var result = await c.GetAsync("https://www.bing.com");
result.EnsureSuccessStatusCode();
Console.WriteLine($"After http call: {Process.GetCurrentProcess().Threads.Count}");

如果我在发布运行(或发布运行后)运行此程序.cs,结果是:dotnet run

Start process: 8
After http call: 22

所以。。。在几行代码之后,我有 8 个线程,在 httpcall 22 之后。我只想减少这个数字(如果可能的话),因为我正在运行这个程序的大量实例。


请记住:问题是 linux 有一个限制,我必须减少我的应用程序使用的线程数量。 在 Windows 上没有问题,或者如果我增加 linux 限制。 我的应用程序的一个实例现在运行大约 30-40 秒:

  • http 调用花费的时间:~0.5 秒
  • 从 redis 读取数据所花费的时间:~3 秒。
  • 剩余时间在 CPU 上。

Rider 使用上述代码显示:

  • 3 未知 ?
  • 3 个 ThreadPool 工作线程
  • 1 ThreadPool 等待
  • 1 个 ThreadPool IO
  • 1 个 ThreadPool 门
  • 1 .NET 计时器
  • 1 个主线程
  • 如何查看其他 26-11=15 个线程?更重要的是,如何减少它们?
.NET 多线程 NET-7.0

评论

0赞 Panagiotis Kanavos 11/1/2023
OOM 通常是由低效编码引起的,而不是由多个线程引起的。将大量项逐个添加到列表中可能会导致内存碎片,以至于运行时无法找到单个连续的内存块,以便在列表缓冲区已满后重新分配列表的缓冲区。限制线程只会延迟问题
0赞 Panagiotis Kanavos 11/1/2023
发布你的代码,不要以为这是关于线程的。 使用线程池中的线程,该线程池可以轻松处理 1000 个线程而不会出现问题。async/await
0赞 Sierrodc 11/2/2023
我能够重现“OutOfMemoryException”,只需在 linux 机器中使用“ulimit -u = 4096”手动创建 4096+ 线程。今天,我们已将此限制增加到 16384,并且在创建 16k+ 线程后引发 OutOfMemoryException。
0赞 Panagiotis Kanavos 11/2/2023
我可以通过在紧密循环中分配 1MB 数组来用 1 个线程重现这一点。另一方面,尝试 4K 并发套接字可能会导致大多数机器出现网络问题。人为的错误代码总是会导致崩溃。为什么一个普通的应用程序一开始就会有 4K 线程池线程?这些都是可以重复使用的。这意味着应用程序正在阻止它们
1赞 Hans Passant 11/2/2023
请务必将 ThreadPool.ThreadCount 与 Process.Threads 区分开来。后者还计算不执行任何托管代码的线程。例如用于处理这些 GetAsync() 调用的任何本机线程。更有可能的来源是,所示代码不会对线程池施加任何负载。使用非托管调试器查看它们的作用。

答:

2赞 Stephen Cleary 10/31/2023 #1

其中大多数将是线程池线程,你的朋友也是如此。.NET 历来以桌面/服务器环境为目标,目的是使用可用的计算资源,因此以受约束的执行环境为目标确实需要进行调整。ThreadPool.SetMaxThreads

评论

0赞 Panagiotis Kanavos 11/1/2023
没有理由假设只有 40 个线程会导致 OOM,因为线程池限制是 1000,因为 4.6 甚至更早。还有别的事情正在发生。甚至不应该有 40 个线程,除非有什么东西阻塞了它们
0赞 Sierrodc 11/2/2023
我添加了几行代码。即使使用 ThreadPool.SetMaxThreads(1, 1);在 http get 之后,我有 20+ 线程在运行。
0赞 Panagiotis Kanavos 11/2/2023
@Sierrodc这几行如果同时执行,会泄漏套接字和 HttpClient 实例
0赞 Sierrodc 11/7/2023 #2

部分响应。

我无法使用 .ThreadPool.SetMaxThreads

可以配置为使用 ThreadPool 中的线程,而不是专用的线程池(由 10 个线程组成):StackExchange.Redis

    var configurationOptions = new ConfigurationOptions
    {
        SocketManager = SocketManager.ThreadPool
    };

因此,我能够减少 8 个线程。