繁体   English   中英

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

[英]What happens if you start too many threads?

创建太多线程会发生什么? 它会导致 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% 中,它可能有点意义,但您很可能应该使用 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 使用了“句柄”的概念。 每个窗口、每个线程、每个共享内存块、每个打开的文件流等,只要它是活动的(并且更长一点) - 都有一个唯一的句柄。 您实际上可以通过用完所有句柄来使窗口处于 STARVE 状态

例如,如果您用完所有 GUI 句柄,您将无法打开新窗口。 或窗口区域。 或者控制。 想象一下打开一个记事本,它启动后不显示 Menu 和 TextArea,因为没有足够的空闲句柄来分配它们。

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

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

我不是这方面的真正专家,让我们回到专家写的一系列文章,他们在那里隐藏了线程限制、处理限制等等:

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

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

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

但是如果你打算使用很多线程,请考虑使用线程池作为重用底层线程对象的一种方式(同时有很多可运行的 - 只是不运行)。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM