如果启动太多线程会怎样?

What happens if you start too many threads?

创建过多线程会怎样?它会导致 CPU 崩溃还是 Windows OS' 上有某种内部负载平衡机制?

我是运行以下代码:

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 只执行 OS 告诉它的,OS 是 in charge 的线程 运行 以及它们 运行 在他们被打断之前。调度程序中内置了一些反饥饿机制,因此它永远不会完全锁定系统,但如果你只是继续生成尽可能多的线程,直到 运行 内存或地址不足,你几乎可以让它屈服space.

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

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

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

首先,为什么 运行 来自任务中的线程?在 99.9% 的情况下,这是没有意义的。在剩下的 0.1% 中,它可能有点意义,但您很可能应该使用 TaskCompletionSource 而不是 Task。

任务的设计使您可以拥有将这些任务排队的调度程序,监视这些任务何时 sleep/wait/etc 并同时将线程重用到 运行 其他任务..

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

调度器不是魔法,他们没有 crystal 球来预测未来。我说它们 "decide",但这只对了一半:调度程序通常根据其种类遵循一些通用规则。因此,您选择适合您的幻想的调度程序并完成。

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

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

说真的。如果你不想 read/etc,那么只需要阅读第一篇文章并阅读不同调度程序的 names 至少可以了解你选择了多少种可能性忽略。

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

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

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

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

还有一个硬性限制。为了跟踪重要的对象,Windows 使用了 "handles" 的概念。每个 window、每个线程、每个共享内存块、每个打开的文件流等,只要它还活着(并且更长一点)——都有一个唯一的句柄。你实际上可以 STARVE windows 用完所有句柄。

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

由于该限制,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/) 是一种更有效的策略。

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