多线程:线程多于内核有啥意义?

Posted

技术标签:

【中文标题】多线程:线程多于内核有啥意义?【英文标题】:Multithreading: What is the point of more threads than cores?多线程:线程多于内核有什么意义? 【发布时间】:2011-03-08 18:09:53 【问题描述】:

我认为多核计算机的意义在于它可以同时运行多个线程。在那种情况下,如果你有一台四核机器,那么同时运行 4 个以上的线程又有什么意义呢?他们不会只是在互相窃取时间(CPU 资源)吗?

【问题讨论】:

我们喜欢这种类型的问题,他们质疑事物的根本,这被认为是理所当然的......继续...... 您上一次在四核机器上同时运行 Firefox、MS Word、Winamp、Eclipse 和下载管理器(超过四个程序/进程)是什么时候?此外,单个应用程序有时可能会产生四个以上的线程 - 那怎么样? 偷窃不一定是坏事。对于需要占用时间的重要任务,您可能有一个优先级更高的线程。 @Amarghosh 我想这就是问题所在,如果单个应用程序似乎没有带来任何性能优势,为什么它可能希望产生比内核更多的线程。你的例子有四个以上的程序在这里不太相关。正如您正确指出的那样,这些是过程。操作系统多任务功能(进程多路复用)与一个进程中的线程几乎没有关系。 【参考方案1】:

答案围绕线程的目的,即并行性:一次运行多个单独的执行行。在一个“理想”系统中,每个内核都会执行一个线程:没有中断。事实上,情况并非如此。即使您有四个内核和四个工作线程,您的进程和它的线程也会不断地被其​​他进程和线程切换。如果您正在运行任何现代操作系统,那么每个进程都至少有一个线程,而且许多线程还有更多。所有这些进程同时运行。你现在可能有几百个线程都在你的机器上运行。您永远不会遇到线程在没有时间“偷走”的情况下运行的情况。 (好吧,如果是 running real-time,如果您使用的是实时操作系统,或者即使在 Windows 上,也可能使用实时线程优先级。但这种情况很少见。)

以此为背景,答案是:是的,真正的四核机器上超过四个线程可能会给您一种“相互窃取时间”的情况,但前提是每个单独的线程需要 100 % CPU。如果一个线程没有 100% 工作(因为一个 UI 线程可能没有工作,或者一个线程做少量工作或等待其他事情),那么另一个线程被调度实际上是一个很好的情况。

实际上比这更复杂:

如果您需要同时完成五项工作怎么办?一次运行它们比运行其中四个然后再运行第五个更有意义。

很少有线程真正需要 100% CPU。例如,在它使用磁盘或网络 I/O 的那一刻,它可能会花时间等待无所事事。这是很常见的情况。

如果您有需要运行的工作,一种常见的机制是使用线程池。拥有与内核相同数量的线程似乎是有意义的,但the .Net threadpool has up to 250 threads available per processor。我不确定他们为什么这样做,但我的猜测是与线程上运行的任务的大小有关。

所以:偷时间不是一件坏事(也不是真正的偷窃:这是系统应该如何工作的。)根据线程将执行的工作类型编写多线程程序,这可能不受 CPU 限制。根据分析和测量确定您需要的线程数。您可能会发现考虑任务或作业而不是线程更有用:编写工作对象并将它们提供给要运行的池。最后,除非您的程序真正对性能至关重要,否则不要太担心:)

【讨论】:

+1 表示“但前提是每个单独的线程都需要 100% 的 CPU”。这是我没有意识到自己在做的假设。 整体答案很好。我缺少的一件事是提到了“中断信号”和“上下文切换”这两个术语。在我看来,两者都是理解上述内容的基础。【参考方案2】:

仅仅因为一个线程存在并不总是意味着它正在积极运行。许多线程应用程序涉及一些线程进入休眠状态,直到它们需要执行某些操作 - 例如,用户输入触发线程唤醒、执行某些处理并返回休眠状态。

从本质上讲,线程是可以相互独立运行的单独任务,无需了解另一项任务的进度。很可能拥有比同时运行的能力更多的这些;即使他们有时必须排成一列,它们仍然可以方便地使用。

【讨论】:

说得好。 “每个 CPU 一个线程”参数仅适用于 CPU 绑定代码。异步编程是使用线程的另一个原因。【参考方案3】:

关键是,尽管当线程数超过核心数时没有得到任何真正的加速,但您可以使用线程来解开不应相互依赖的逻辑片段。

即使在一个中等复杂的应用程序中,使用单个线程尝试快速完成所有事情都会使代码的“流”散列。单线程大部分时间都在轮询这个、检查那个、根据需要有条件地调用例程,除了一堆细枝末节之外,很难看到任何东西。

将此与您可以将线程专用于任务的情况进行对比,以便查看任何单个线程,您可以看到该线程正在做什么。例如,一个线程可能会阻塞等待来自套接字的输入,将流解析为消息,过滤消息,当出现有效消息时,将其传递给其他工作线程。工作线程可以处理来自许多其他来源的输入。这些代码中的每一个都将展示一个干净、有目的的流程,而无需明确检查是否有其他事情可做。

以这种方式对工作进行分区允许您的应用程序依赖操作系统来安排接下来要使用 cpu 执行的操作,因此您不必在应用程序的任何地方都进行明确的条件检查,以了解哪些内容可能会阻塞以及哪些内容已准备好过程。

【讨论】:

这是一个有趣的想法...我一直听说多线程应用程序会增加复杂性,但您说的很有道理。 如果应用程序的关注点没有充分分离,多线程会增加复杂性。如果它的设计具有最小的关注点重叠(因此共享状态),那么它可以节省复杂性问题。 有一些方法可以构建单线程应用程序,以便在您编写程序的级别上控制流更加清晰。 OTOH,如果您可以构建您的线程,以便它们仅将消息传递给彼此(而不是共享资源),那么弄清楚发生了什么并让一切正常工作非常简单。 不过应该​​指出,使用线程只能在一定程度上简化事情。经常尝试让两个线程完成应该由一个线程正确完成的工作,而复杂性又回来了。这种情况的症状是过度需要沟通和同步,以协调一些期望的结果。 我认为如果线程数 > 内核数,说我们没有得到“任何真正的加速”是一种误导。这根本不是真的。正如其他答案所述,由于在等待 I/O 或其他任何事情时线程空闲时间的智能上下文切换,可以通过使用比内核更多的线程来实现显着的性能改进。【参考方案4】:

如果线程正在等待资源(例如从 RAM 加载值到寄存器、磁盘 I/O、网络访问、启动新进程、查询数据库或等待用户输入),处理器可以在不同的线程上工作,并在资源可用后返回第一个线程。这减少了 CPU 空闲的时间,因为 CPU 可以执行数百万次操作而不是处于空闲状态。

考虑一个需要从硬盘读取数据的线程。 2014 年,一个典型的处理器内核运行频率为 2.5 GHz,每个周期可能能够执行 4 条指令。凭借 0.4 ns 的周期时间,处理器每纳秒可以执行 10 条指令。典型的机械硬盘寻道时间约为 10 毫秒,处理器能够在从硬盘读取值所需的时间内执行 1 亿条指令。具有小缓存(4 MB 缓冲区)的硬盘驱动器和具有几 GB 存储空间的混合驱动器可能会显着提高性能,因为顺序读取或从混合部分读取的数据延迟可能会快几个数量级。

处理器内核可以在线程之间切换(暂停和恢复线程的成本约为 100 个时钟周期),而第一个线程等待高延迟输入(任何比寄存器(1 个时钟)和 RAM(5 纳秒)更昂贵的东西) ) 这些包括磁盘 I/O、网络访问(延迟 250 毫秒)、从 CD 或慢速总线读取数据或数据库调用。线程多于内核意味着可以在解决高延迟任务的同时完成有用的工作。

CPU 有一个线程调度器,它为每个线程分配优先级,并允许线程休眠,然后在预定时间后恢复。减少抖动是线程调度程序的工作,如果每个线程在再次进入睡眠状态之前只执行了 100 条指令,就会发生这种情况。切换线程的开销会降低处理器内核的总有用吞吐量。

因此,您可能希望将问题分解为合理数量的线程。如果您正在编写代码来执行矩阵乘法,则在输出矩阵中的每个单元格创建一个线程可能会过多,而在输出矩阵中每行或每 n 行创建一个线程可能会降低创建的开销成本、暂停和恢复线程。

这也是分支预测很重要的原因。如果您有一条 if 语句需要从 RAM 加载一个值,但 if 和 else 语句的主体使用已加载到寄存器中的值,则处理器可能会在评估条件之前执行一个或两个分支。一旦条件返回,处理器将应用相应分支的结果并丢弃另一个。在这里执行可能无用的工作可能比切换到其他线程更好,这可能会导致抖动。

随着我们从高时钟速度的单核处理器转向多核处理器,芯片设计的重点是在每个裸片上塞进更多的内核、改进内核之间的片上资源共享、更好的分支预测算法、更好的线程切换开销,以及更好的线程调度。

【讨论】:

同样的事情可以用一个线程和一个队列来完成:\ 在 2-4 个内核上拥有 80 个线程真的有什么好处,而不是仅仅拥有 2-4 个内核来吃掉任务他们一到就排队,他们无事可做?【参考方案5】:

上面的大多数答案都在谈论性能和同时操作。我将从不同的角度来解决这个问题。

让我们以一个简单的终端仿真程序为例。你必须做以下事情:

监视来自远程系统的传入字符并显示它们 注意来自键盘的内容并将它们发送到远程系统

(真正的终端模拟器可以做更多的事情,包括可能会将您键入的内容回显到显示器上,但我们现在将忽略它。)

现在从远程读取的循环很简单,根据以下伪代码:

while get-character-from-remote:
    print-to-screen character

监听键盘和发送的循环也很简单:

while get-character-from-keyboard:
    send-to-remote character

但问题是您必须同时执行此操作。如果没有线程,代码现在必须看起来更像这样:

loop:
    check-for-remote-character
    if remote-character-is-ready:
        print-to-screen character
    check-for-keyboard-entry
    if keyboard-is-ready:
        send-to-remote character

即使在这个故意简化的示例中,没有考虑到通信的实际复杂性,其逻辑也相当模糊。然而,使用线程,即使在单个内核上,两个伪代码循环也可以独立存在,而不会交织它们的逻辑。由于这两个线程大部分都是 I/O 密集型的,因此它们不会给 CPU 带来沉重的负担,尽管严格来说,它们比集成循环更浪费 CPU 资源。

现在,现实世界的使用当然比上面的要复杂。但是,当您向应用程序添加更多关注点时,集成循环的复杂性会呈指数级上升。逻辑变得越来越碎片化,你必须开始使用状态机、协程等技术来让事情变得易于管理。可管理,但不可读。线程使代码更具可读性。

那你为什么不使用线程呢?

好吧,如果您的任务是 CPU 密集型而不是 I/O 密集型,那么线程实际上会减慢您的系统速度。性能会受到影响。很多,在很多情况下。 (“抖动”是一个常见问题,如果您丢弃太多 CPU 绑定线程。您最终会花费更多时间更改活动线程而不是运行线程本身的内容。)此外,上述逻辑的原因之一是如此简单,以至于我特意选择了一个简单(且不切实际)的示例。如果您想在屏幕上回显输入的内容,那么当您引入共享资源的锁定时,您将获得一个新的伤害世界。只有一个共享资源这不是什么大问题,但随着您有更多资源要共享,它确实开始成为一个越来越大的问题。

所以说到底,线程是关于很多事情的。例如,正如一些人已经说过的,它是关于使 I/O 绑定的进程更具响应性(即使整体效率较低)。这也是为了让逻辑更容易理解(但前提是你最小化共享状态)。它涉及很多东西,你必须根据具体情况决定它的优点是否大于缺点。

【讨论】:

【参考方案6】:

虽然您当然可以根据您的硬件使用线程来加快计算速度,但出于用户友好性的原因,线程的主要用途之一是一次做不止一件事。

例如,如果您必须在后台进行一些处理并保持对 UI 输入的响应,则可以使用线程。如果没有线程,每次您尝试进行繁重的处理时,用户界面都会挂起。

另请参阅此相关问题:Practical uses for threads

【讨论】:

UI 处理是 IO 绑定任务的经典示例。单个 CPU 内核同时执行处理和 IO 任务并不好。【参考方案7】:

我非常不同意 @kyoryu 的说法,即理想的数量是每个 CPU 一个线程。

这样想:为什么我们有多处理操作系统?在计算机历史的大部分时间里,几乎所有计算机都有一个 CPU。然而,从 1960 年代开始,所有“真正的”计算机都具有多处理(也称为多任务)操作系统。

您运行多个程序,以便一个可以运行,而其他程序则因 IO 等原因而被阻止。

让我们搁置 NT 之前的 Windows 版本是否是多任务处理的争论。从那时起,每个真正的操作系统都有多任务处理。有些人不会将它暴露给用户,但它仍然存在,例如收听手机收音机、与 GPS 芯片交谈、接受鼠标输入等。

线程只是更高效的任务。任务、进程和线程之间没有根本区别。

CPU 是一种非常浪费的东西,所以尽可能多地准备好使用它。

我同意对于大多数过程语言,C、C++、Java 等,编写适当的线程安全代码是一项繁重的工作。目前市场上有 6 核 CPU,而 16 核 CPU 也不远了,我希望人们会远离这些旧语言,因为多线程越来越成为一项关键要求。

不同意@kyoryu 只是恕我直言,其余的都是事实。

【讨论】:

如果你有很多 处理器绑定 线程,那么理想的数量是每个 CPU 一个(或者可能少一个,留一个来管理所有 I/ O和操作系统以及所有这些东西)。如果你有 IO-bound 线程,你可以在单个 CPU 上堆叠很多。不同的应用程序具有不同的处理器绑定和 IO 绑定任务组合;这很自然,但为什么你必须小心通用声明。 当然,线程和进程最重要的区别在于Windows上没有fork(),所以创建进程真的很费钱,导致线程的过度使用。 除了蛋白质折叠、SETI 等之外,没有任何实际的用户任务需要长时间计算。总是需要从用户那里获取信息,与磁盘通信,与 DBMS 通信等。是的,fork() 的开销是 Cutler 诅咒 NT 的众多事情之一,而 DEC 的其他人都知道。 【参考方案8】:

想象一个必须为任意数量的请求提供服务的 Web 服务器。您必须并行处理请求,否则每个新请求都必须等到所有其他请求都完成(包括通过 Internet 发送响应)。在这种情况下,大多数 Web 服务器的内核数量远远少于它们通常服务的请求数量。

这也让服务器的开发者更容易:你只需要编写一个线程程序来服务一个请求,你不必考虑存储多个请求,你服务它们的顺序等等。

【讨论】:

您正在为支持线程但没有多路复用能力的操作系统编写软件?我认为 Web 服务器可能是一个不好的例子,因为在这种情况下,多路复用 io 几乎总是比产生比核心更多的线程更有效。【参考方案9】:

许多线程将处于休眠状态,等待用户输入、I/O 和其他事件。

【讨论】:

当然。只需在 Windows 上使用任务管理器或在真实操作系统上使用 TOP,然后查看还有多少任务/进程正在运行。它总是 90% 或更多。【参考方案10】:

线程有助于提高 UI 应用程序的响应能力。此外,您可以使用线程从内核中获得更多工作。例如,在单个内核上,您可以让一个线程执行 IO,而另一个线程执行一些计算。如果它是单线程的,那么内核基本上可以空闲等待 IO 完成。这是一个相当高级的例子,但是线程绝对可以用来给你的 CPU 带来更多的压力。

【讨论】:

更具体地说,一个线程可以在 I/O 上等待,而另一个线程进行计算。如果 I/O 占用了(大量的)CPU 周期,那么在单独的线程中运行它没有任何好处。好处是你的计算线程可以运行,而你的 I/O 线程正在旋转它的拇指,等待一个大的铝圆柱体旋转到位,或者等待数据包从冰岛通过电线到达,或者其他什么。【参考方案11】:

处理器或 CPU 是插入系统的物理芯片。一个处理器可以有多个内核(内核是芯片中能够执行指令的部分)。如果一个内核能够同时执行多个线程(线程是一个单一的指令序列),那么它在操作系统中可以表现为多个虚拟处理器。

进程是应用程序的另一个名称。通常,进程是相互独立的。如果一个进程死亡,它不会导致另一个进程也死亡。进程可以进行通信,或共享内存或 I/O 等资源。

每个进程都有一个单独的地址空间和堆栈。一个进程可以包含多个线程,每个线程能够同时执行指令。一个进程中的所有线程共享相同的地址空间,但每个线程都有自己的堆栈。

希望这些定义和使用这些基础知识的进一步研究将有助于您理解。

【讨论】:

我根本不明白这是如何解决他的问题的。我对他的问题的解释是关于内核的线程使用和可用资源的最佳使用,或者关于线程数量增加时的行为,或者无论如何。 @David 也许这不是我问题的直接答案,但我仍然觉得我通过阅读它学到了。【参考方案12】:

按照某些 API 的设计方式,您别无选择只能在单独的线程中运行它们(任何具有阻塞操作的线程)。一个例子是 Python 的 HTTP 库 (AFAIK)。

不过,通常这不是什么大问题(如果有问题,操作系统或 API 应该附带另一种异步操作模式,即:select(2)),因为这可能意味着线程将在等待 I/O 完成期间休眠。另一方面,如果某项计算量很大,您必须将它放在一个单独的线程中,而不是 GUI 线程(除非您喜欢手动多路复用)。

【讨论】:

【参考方案13】:

线程的理想用法确实是每个内核一个。

但是,除非您专门使用异步/非阻塞 IO,否则您很有可能会在某个时候在 IO 上阻塞线程,这不会使用您的 CPU。

此外,典型的编程语言使得每个 CPU 使用 1 个线程有些困难。围绕并发设计的语言(例如 Erlang)可以更轻松地不使用额外的线程。

【讨论】:

使用线程来执行周期性任务是一种非常普遍且受欢迎的工作流程,如果他们偷了一个核心,那就不太理想了。 @Nick Bastin:是的,但是将这些任务放入任务队列并从该队列执行它们(或类似策略)更有效。为了获得最佳效率,每个核心 1 个线程胜过一切,因为它可以防止不必要的上下文切换和分配额外堆栈造成的开销。无论如何,周期性任务必须在“活动”时窃取一个核心,因为 CPU 实际上每个核心只能执行一个任务(如果可用,还可以加上超线程等东西)。 @Nick Bastin:不幸的是,正如我在主要答案中所说,大多数现代语言都不适合轻松实现有效执行此操作的系统并非微不足道 - 你最终会做一些事情与语言的典型用法作斗争。 我的意思不是每个核心一个线程不是最佳的,而是每个核心一个线程是一个白日梦(除非你是嵌入式的)并且设计试图达到它是一种浪费时间,所以你最好做一些对你来说很容易的事情(并且无论如何在现代调度程序上的效率并没有降低),而不是尝试优化你正在使用的线程数。我们应该无缘无故地启动线程吗?当然不是,但是无论线程如何,您是否会不必要地浪费计算机资源都是一个问题。 @Nick Bastin:总而言之,每个内核一个线程是理想的,但实际上实现这一点的可能性不大。在谈到实际实现这样的事情的可能性时,我可能应该比“有点困难”更强。【参考方案14】:

回应您的第一个猜想:多核机器可以同时运行多个进程,而不仅仅是单个进程的多个线程。

回答您的第一个问题:多线程的意义通常是在一个应用程序中同时执行多个任务。网络上的经典例子是一个发送和接收邮件的电子邮件程序,以及一个接收和发送页面请求的网络服务器。 (请注意,基本上不可能将像 Windows 这样的系统减少到只运行一个线程甚至只运行一个进程。运行 Windows 任务管理器,您通常会看到一长串活动进程,其中许多将运行多个线程。 )

回答您的第二个问题:大多数进程/线程不受 CPU 限制(即,不连续不间断地运行),而是经常停止并等待 I/O 完成。在该等待期间,其他进程/线程可以在不从等待代码“窃取”的情况下运行(即使在单核机器上)。

【讨论】:

【参考方案15】:

我知道这是一个非常古老的问题,有很多很好的答案,但我在这里指出一些在当前环境中很重要的事情:

如果您想为多线程设计应用程序,则不应针对特定的硬件设置进行设计。多年来,CPU 技术一直在快速发展,核心数量也在稳步增加。如果您故意将应用程序设计为仅使用 4 个线程,那么您可能会将自己限制在八核系统中(例如)。现在,即使是 20 核的系统也已经商用,所以这样的设计肯定弊大于利。

【讨论】:

【参考方案16】:

线程是一种抽象,它使您能够编写像一系列操作一样简单的代码,完全不知道代码与其他代码交错执行,或者等待 IO,或者(可能更清楚)等待用于其他线程的事件或消息。

【讨论】:

自反对票以来,我可能已经通过添加更多示例来编辑它 - 但是线程(或进程,在这种情况下几乎没有区别)并不是为了提高性能而发明的,而是为了简化异步代码并避免编写必须处理程序中所有可能的超状态的复杂状态机。事实上,即使在大型服务器中也通常只有一个 CPU。我只是好奇为什么我的回答被认为是反帮助的?【参考方案17】:

关键是绝大多数程序员不了解如何设计状态机。能够将所有内容放在自己的线程中,程序员就不必考虑如何有效地表示不同正在进行的计算的状态,以便它们可以被中断并在以后恢复。

以视频压缩为例,这是一项占用大量 CPU 资源的任务。如果您使用的是 gui 工具,您可能希望界面保持响应(显示进度、响应取消请求、调整窗口大小等)。因此,您将编码器软件设计为一次处理一个大型单元(一个或多个帧)并在其自己的线程中运行,与 UI 分开。

当然,一旦您意识到能够保存正在进行的编码状态以便您可以关闭程序以重新启动或玩需要资源的游戏会很好,您就会意识到您应该已经学会了如何设计状态从一开始的机器。要么这样,要么你决定设计一个全新的进程休眠问题,让你的操作系统可以暂停和恢复单个应用程序到磁盘...

【讨论】:

不(相当!)值得-1,但说真的,这是我听过任何人在这个问题上说的最愚蠢的讽刺的话。例如,我在实现状态机方面没有问题。一个都没有。当其他工具留下更清晰更易于维护代码时,我只是不喜欢使用它们。状态机有它们的位置,在那些地方它们是无法匹配的。将 CPU 密集型操作与 GUI 更新交错并不是其中之一。至少协同程序是一个更好的选择,线程更好。 对于所有修改我的答案的人来说,这不是反对使用线程的论据!如果您可以编写一个很棒的状态机,并且确保在单独的线程中运行状态机通常是有意义的,即使您不必这样做。我的评论是,通常选择使用线程主要是为了避免设计状态机,许多程序员认为这“太难”了,而不是为了任何其他好处。

以上是关于多线程:线程多于内核有啥意义?的主要内容,如果未能解决你的问题,请参考以下文章

多线程 - 多线程基础

JAVA并发编程揭开篇章,并发编程基本认识,了解多线程意义和使用

JAVA并发编程揭开篇章,并发编程基本认识,了解多线程意义和使用

C# 到 C++ 多线程,有啥问题吗?

你真的知道什么是多线程吗?为什么要学习多线程?

golang的线程模型——GMP模型