如果启动的线程过多会发生什么情况



创建太多线程时会发生什么?它是否会导致CPU崩溃,或者Windows操作系统上是否有某种内部负载平衡机制?

我正在运行以下代码:

private async void A(string[] a)
{
var tasks = a.Select(B);
await Task.WhenAll(tasks);
}
private async Task B(string b)
{
new Thread(async delegate ()
{
//all the work that needs to be done
}).Start();
}

我正在运行一个异步任务数组,但在每个异步方法中,我已经将所有需要完成的工作封装在新线程中。如果我多次打电话给 B 会怎样?处理器将如何处理过多的线程?

CPU只执行操作系统告诉它的内容,操作系统负责哪些线程运行以及它们在中断之前运行多长时间。调度程序中内置了一些抗饥饿功能,因此它永远不会完全锁定系统,但是如果您继续生成尽可能多的线程直到耗尽内存或地址空间,则几乎可以使其瘫痪。

如果我们假设您的程序是唯一正在运行的程序,那么如果任务受 CPU 限制,则理想的线程数与 CPU 内核数相同。如果任务受 I/O 限制或需要等待内核对象,则更多线程可能是理想的。

如果您创建了数千个线程,那么您将浪费时间在它们之间切换上下文,并且您的工作将需要更长的时间才能完成。与其手动启动新线程,不如使用线程池来执行工作,以便 Windows 本身可以平衡最佳线程数。

await和其他高级异步关键字可能已经在使用线程池。

首先,为什么要从任务中运行线程?在99.9%的情况下,这毫无意义。在剩下的0.1%中,maaybe有点意义,但你很可能应该使用TaskCompletionSource而不是Task。

任务的设计是为了让你有调度程序对这些任务进行排队,监控这些任务何时休眠/等待/等,并重用线程来同时运行其他任务。

基本上,您将"工作"包装到任务中,然后将这些任务交给调度程序,然后调度程序决定是否、何时以及运行多少线程来执行这些任务。

调度器不是魔法,它们没有水晶球来预测未来。我说他们"决定",但这只对了一半:调度程序通常遵循一些一般规则,具体取决于其种类。因此,您可以为您的幻想选择合适的调度程序并完成。

说真的,放弃目前的方法。请改用调度程序。您甚至可以拥有一个调度程序,它将在单独的线程上执行每个任务。它将等效于您当前的方法。但是,您将能够快速切换到另一个调度程序并感受到差异。

这里有一些资源给你,一个非常重要的库:

  • 许多其他类型的调度程序开箱即用
  • 进入提供它们的库
  • Nuget with the lib

认真地。如果你不想阅读/等,那么只需阅读第一篇文章,只阅读不同调度程序的名称,至少可以了解你选择忽略多少种可能性。

最后,回答这个问题,是的,Windows在某种程度上是负载平衡的。它将尝试防止运行过多线程。它实际上会在给定的时间点运行少量线程(或多或少等于处理器中的逻辑执行单元数),其余线程将休眠并等待它们的时间。Windows偶尔会在它们之间切换,因此您会观察到好像它们都在运行,但是其中一些速度较慢,其中一些速度更快。

但是,这并不意味着您可以创建无限数量的线程。显然,内存限制是有的:如果你有 X GB 的内存,你不能保留比内存所能容纳的更多的内存。我现在有点开玩笑,但既然有一些明显的限制,就会有更多的限制。但是,这里有一点严肃性,因为,你看,每个线程都有一个 STACK,并且该堆栈可以按兆字节的顺序排列,所以如果你有 32 位处理器,STACK 的数量最多可以达到几千个。所以。。是的,内存可能是一个限制。这在 64 位上不太明显,但是,可以肯定的是,您没有足够的 RAM 来填充整个 64 位地址空间,因此在 64 位上您也会有限制。

由于Windows将尝试保留所有线程的记录,即使是那些休眠线程,因此它将浪费时间跟踪这些记录。此外,它会在切换上浪费时间,因为作为操作系统,它会尝试让它们都旋转和运行。这直接意味着您创建的线程越多(1/10/100/1000/..),一切都会运行得更慢 - 并且比除以 N 个线程(不是:1/0.1/0.01/0.001/..,而是:1/0.1/0.097/0.0089/..)慢,因为浪费在保存记录和切换上的时间。

线程也有优先级。内部系统线程通常具有更高的优先级。系统将比切换到您的更频繁地切换到它们,这意味着您运行的线程越多,您的应用程序处理速度就越慢。

还有一个硬性限制。为了跟踪重要对象,Windows 使用"句柄"的概念。每个窗口,每个线程,每个共享内存块,每个打开的文件流等,只要它处于活动状态(并且更长一点) - 都有一个唯一的句柄。您实际上可以通过用完所有手柄来饿死窗口。

例如,如果用完所有 GUI 句柄,将无法打开新窗口。或窗口区域。或控件。想象一下,打开一个记事本,它启动时没有显示菜单和文本区域,因为没有足够的可用句柄来分配它们。

由于该限制,Windows 实际上限制了每个进程分配的句柄数。这意味着,比如说,Windows有一个1M句柄的池,但每个进程最多只能使用1K。这些数字是人为的,只是为了让你得到一个想法。

由于物理(本机)线程必须具有句柄,因此这是另一个限制。

我不是这方面的真正专家,让我们回到专家撰写的一系列文章,它们隐蔽了线程限制、句柄限制等等:

https://blogs.technet.microsoft.com/markrussinovich/2009/07/05/pushing-the-limits-of-windows-processes-and-threads/

线程确实有很大的成本 - 非常粗略 - 想象一下每个线程100K字节(它们每个都需要一个堆栈来做一件事),并且它们每个都对必须管理它们的操作系统组件(例如调度程序)施加了轻微的麻烦。

线程确实提供了一个非常简单的模型来管理异步任务。我是这种方法的忠实粉丝。

但是,如果您要使用大量线程,请考虑使用线程池作为重用底层线程对象的一种方法(同时具有大量可运行对象 - 只是不运行)。

而且 - 由于您使用的是 C#,异步任务 (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) 是一种更有效的策略。

通常,实现的简单性比效率更重要(在一定程度上)。您使用线程池描述的内容(限制实际线程计数)可能工作正常。

相关内容

最新更新