如果启动过多线程会发生什么?

3
当你创建太多线程会发生什么?会导致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会发生什么?处理器如何处理过多的线程?


这可能非常取决于“需要完成的所有工作”。 - spender
2
操作系统的工作是决定处理器应该处理什么。您可以创建多少线程受到通常资源(内存)的限制。这个数字远远高于程序员能够调试的数字。通常的经验是:如果你需要知道,那么你做错了。 - Hans Passant
1
简短的回答是:如果您启动了太多的线程,性能将会下降,不仅会影响您的应用程序,还会影响整个系统。 - Harry Johnston
1
这篇文章包含一些有趣的相关信息:The Old New Thing: Windows每个进程是否有2000个线程的限制? - Michael Burr
3个回答

5
CPU仅执行操作系统指示的内容,操作系统负责管理线程何时运行以及运行多长时间后被中断。调度程序内置了一些反饥饿机制,因此它不应完全锁定系统,但是如果您不断生成尽可能多的线程直到耗尽内存或地址空间,则可以几乎使其崩溃。
如果我们假设您的程序是唯一运行的程序,则在任务受限于CPU时,理想的线程数量与CPU核心数相同。如果任务受限于I/O或需要等待内核对象,则可能需要更多线程。
如果创建成千上万个线程,则会浪费时间在它们之间进行上下文切换,并且工作将需要更长时间才能完成。而不是手动启动新线程,您应该使用线程池来执行工作,这样Windows本身就可以平衡最佳线程数量。
“await”和其他高级异步关键字可能已经使用线程池。

是的,事实上它们默认使用线程池,但实际上 await 和相关操作基于 Task(而非 Thread),因此可以使它们使用任何您想要的 TaskScheduler。实际上,Task 更易于使用和配置,并且比线程更轻量级,所以在 .Net 上下文中,通常不应该触及“Threads”或池,除非您想为某些原因创建自己的调度程序... 而通过 ParallelExtensionsExtras 库及其各种调度程序,你不太可能需要编写新的调度程序。啊,是的,我是从桌面视角发言的... - quetzalcoatl
正确的异步代码不会阻塞线程,await语句除外。真正的异步代码根本不会阻塞线程 - Stephen Cleary
@StephenCleary:你指的是什么?没有人说过await会阻塞..(虽然有时可能会死锁:P)你是指Anders的最后一行吗?await将方法分成“头”和一个/多个“续体”,而前者在调用线程上运行,后者不必如此,并且可以在您配置它们的任何上下文中运行 - 原始调用方、线程池、您自己的调度程序 - 因此说“await使用线程池”并不完全不正确。这只是不精确的,并且只是完整故事的一部分。还是你指的是其他什么? - quetzalcoatl
@quetzalcoatl: 我指的是答案中的最后一行。关于await只是阻塞线程池线程的想法是一个常见的误解。此外,由async创建的Task(以及大多数被await消耗的任务)与TaskScheduler没有任何关系。 - Stephen Cleary
@StephenCleary:在“await和其他高级异步关键字可能已经使用线程池。”这句话中,你看到了任何阻塞的语句吗?你说过“正确,除了……”我想问一下那个词的意思。 - quetzalcoatl

4
首先,为什么要在任务中运行线程?在99.9%的情况下,这根本没有意义。在剩下的0.1%情况下,也许有一点点意义,但你最好使用TaskCompletionSource而不是Task。
任务的设计是为了让你可以有调度器来排队这些任务,监视这些任务的休眠/等待等状态,并在此期间重用线程来运行其他任务。
基本上,你将你的“工作”包装成任务,然后将这些任务交给调度器,然后调度器决定是否、何时以及运行多少个线程来执行这些任务。
调度器并不是魔法,它们也没有水晶球来预测未来。我说它们“决定”,但这只是一半的真相:调度器通常遵循某些一般规则,具体取决于它的类型。因此,你选择适合你想象力的正确调度器就可以了。
认真地,放弃当前的方法。使用调度器代替。你甚至可以有一个调度器,它将在单独的线程上执行每个任务。这将等同于你目前的方法。但是,你将能够快速切换到另一个调度器并感受到差异。
以下是几个资源,非常重要的库:

如果你不想阅读等等,那么至少阅读第一篇文章并仅阅读不同调度程序的名称,以了解您选择忽略的可能性数量。

最后,回答问题,是的,Windows有一定的负载均衡。它会尝试防止运行过多的线程。实际上,在给定时刻它会运行少量的线程(大致等于处理器中逻辑执行单元的数量),其余的线程会进入睡眠状态等待执行。Windows会不时地在它们之间切换,所以你会觉得好像它们都在运行,但有些线程速度较慢,有些则运行得更快。
然而,这并不意味着你可以创建无限量的线程。显而易见,存在内存限制:如果你有X GB的内存,那么你不能保留比内存容量还多的线程。我现在有点开玩笑了,但因为有明显的极限,所以肯定还会有其他限制。然而,这里有一点严重性,因为每个线程都有一个堆栈,这个堆栈可能有数百万字节,所以如果你的处理器是32位的话,堆栈的数量最多只能达到几千个。所以.. 是的,内存可能是一个限制。在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使用“句柄”的概念。每个窗口、每个线程、每个共享内存块、每个打开的文件流等,只要它还活着(并且再长一点)- 就有一个唯一的句柄。你实际上可以通过使用所有句柄来使Windows“饥饿”。
例如,如果您使用完所有GUI句柄,您将无法打开新窗口。或窗口区域。或控件。想象一下打开一个记事本,它启动并显示没有菜单和没有文本区域,因为没有足够的空闲句柄来分配给它们。
由于该限制,Windows实际上限制了每个进程分配的句柄数量。这意味着,比如说,Windows有一个1M句柄池,但每个进程只能使用最多1K。这些数字是人为设定的,只是为了让您有一个概念。
由于物理(本地)线程必须具有句柄,这里还有另一个限制。
我对这个问题不是真正的专家,所以让我们回到一系列专家撰写的文章,他们涵盖了线程限制、句柄限制等等:

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


1

线程确实有很大的成本 - 非常粗略地说 - 每个线程大约需要100K字节(它们每个都需要一个栈),并且它们都会对操作系统组件(例如调度程序)产生轻微的负担,这些组件必须管理它们。

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

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

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

然而 - 实现的简单性通常比效率更重要(在某种程度上)。您描述的线程池(以限制实际线程数)可能运行良好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接