多线程: 线程多于核心的意义是什么?

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

63754 次浏览

虽然可以根据硬件使用线程来加速计算,但它们的主要用途之一是为了方便用户而一次执行多个操作。

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

也请看这个相关的问题: 线程的实际用途

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

它还使服务器的开发人员变得更加容易: 您只需编写一个服务于请求的线程程序,不必考虑存储多个请求、服务它们的顺序等等。

实际上,线程的理想用法是每个核心一个线程。

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

另外,典型的编程语言使得每个 CPU 使用1个线程有些困难。围绕并发性设计的语言(比如 Erlang)可以使不使用额外线程变得更加容易。

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

本质上,线程是可以彼此独立操作的单个任务,不需要知道另一个任务的进度。它们的数量很可能超过你同时运行的能力; 为了方便起见,它们仍然很有用,即使有时它们不得不排在另一个的后面。

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

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

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

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

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

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

通常情况下,这并不是一个大问题(如果是一个问题,操作系统或 API 应该附带一个可选的异步操作模式,即: select(2)) ,因为这可能意味着线程将在等待 I/O 完成期间处于睡眠状态。另一方面,如果某事正在做一个繁重的计算,你 把它放在一个单独的线程比说,GUI 线程(除非你喜欢手动多路复用)。

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

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

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

通过这种方式对工作进行分区,您的应用程序可以依赖于操作系统来安排 CPU 的下一步操作,这样您就不必在应用程序中的任何地方进行显式的条件检查,以确定哪些内容可能被阻塞,哪些内容可以处理。

答案围绕着线程的目的,即并行性: 同时运行多个独立的执行行。在“理想”系统中,每个核心有一个执行线程: 没有中断。事实并非如此。即使您有四个核心和四个工作线程,您的进程和它的线程也会不断地被切换到其他进程和线程。如果您正在运行任何现代操作系统,每个进程至少有一个线程,许多进程有更多的线程。所有这些进程同时运行。现在您的计算机上可能有几百个线程都在运行。您永远不会遇到这样的情况: 一个线程在运行时没有被“偷走”时间。(如果是 实时运行,如果使用实时操作系统,甚至在 Windows 上,使用实时线程优先级,那么可以这样做。但这种情况很少见。)

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

实际上比这更复杂:

  • 如果你有五件工作需要同时完成怎么办?比起一次运行四个,然后再运行第五个,一次全部运行更有意义。

  • 很少有线程真正需要100% 的 CPU。例如,当它使用磁盘或网络 I/O 时,它可能会花费时间等待,而不做任何有用的事情。这种情况很常见。

  • 如果您有需要运行的工作,一种常见的机制是使用线程池。使用与核心相同数量的线程似乎是有意义的,但 Net 线程池中每个处理器最多有250个线程可用。我不确定他们为什么要这样做,但我猜是因为线程上运行的任务的大小。

所以: 偷取时间不是坏事(也不是真正的偷窃: 它是系统应该如何工作。)根据线程所做的工作类型编写多线程程序,这些工作可能不受 CPU 限制。根据分析和度量,计算出您需要的线程数量。您可能会发现从任务或作业的角度考虑比从线程的角度考虑更有用: 编写工作对象并将它们交给要运行的池。最后,除非您的程序真的是性能关键型的,否则不要太担心:)

我强烈反对@kyoryu 的主张,即理想数字是每个 CPU 一个线程。

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

您可以运行多个程序,这样一个程序可以运行,而其他程序由于 IO 之类的原因而被阻塞。

让我们搁置关于 NT 之前的 Windows 版本是否是多任务的争论。从那时起,每个真正的操作系统都有多任务。有些人不会把它暴露给用户,但它还是在那里,做一些事情,比如听手机广播,与 GPS 芯片通话,接受鼠标输入,等等。

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

一个 CPU 是一个可怕的东西浪费,所以有很多东西准备使用它时,你可以。

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

恕我直言,与@kyoryu 意见不合是事实,其余的都是事实。

上面的大多数答案都是关于性能和同时操作的。我要从另一个角度来看这个问题。

我们假设有一个简单的终端仿真程序,你需要做以下几件事:

  • 监视从远程系统输入的字符并显示它们
  • 注意键盘上的东西,然后把它们发送到远程系统

(真正的终端模拟器做得更多,包括潜在地将您键入的内容回显到显示器上,但是我们现在将忽略这一点。)

现在,从远程读取的循环很简单,如下面的伪代码所示:

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 绑定线程,“ Thrashing”是一个常见问题。更改活动线程所花费的时间比运行线程本身的内容所花费的时间还要多。)此外,上面的逻辑如此简单的原因之一是,我非常谨慎地选择了一个简单(和不现实)的例子。如果您想要回显屏幕上输入的内容,那么当您引入共享资源锁定时,您将面临一个新的伤害世界。只有一个共享的资源,这不是一个问题,但它开始成为一个更大的问题,因为你有更多的资源共享。

所以说到底,线程是关于很多事情的。例如,正如一些人已经说过的,它是关于使 I/O 绑定流程更具响应性(即使整体效率较低)。这也是为了使逻辑更容易遵循(但只有在最小化共享状态的情况下)。它涉及到很多东西,你必须根据具体情况来判断它的优势是否大于劣势。

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

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

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

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

例如,考虑一下视频压缩,这是一项非常耗费 CPU 资源的任务。如果使用 GUI 工具,可能希望界面保持响应(显示进度、响应取消请求、调整窗口大小等)。因此,您可以设计编码器软件,一次处理一个大单元(一个或多个帧) ,并在其自己的线程中运行它,独立于 UI。

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

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

线程是一种抽象,它使您能够编写像操作序列那样简单的代码,幸福地不知道代码是与其他代码交错执行的,或者停在那里等待 IO,或者(可能更多地意识到)等待其他线程的事件或消息。

如果一个线程正在等待一个资源(比如将一个值从 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,网络访问(延迟250ms) ,从 CD 或缓慢总线读取数据,或数据库调用。拥有比核心更多的线程意味着可以在解决高延迟任务时完成有用的工作。

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

出于这个原因,您可能希望将问题分解为合理数量的线程。如果你编写代码来执行矩阵乘法,那么在输出矩阵中每个单元创建一个线程可能有些过分,而在输出矩阵中每行或每 N行创建一个线程可能会降低创建、暂停和恢复线程的开销。

这也是为什么分支预测很重要的原因。如果您有一个 If 语句,需要从 RAM 加载一个值,但是 If 和 else 语句的主体使用已经加载到寄存器中的值,处理器可以在计算条件之前执行一个或两个分支。一旦条件返回,处理器将应用相应分支的结果并丢弃另一个分支。在这里执行可能无用的工作可能比切换到另一个线程要好,后者可能会导致混乱。

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

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

如果要为多线程设计应用程序,则不应为特定的硬件设置进行设计。多年来,CPU 技术发展迅速,核心数量稳步增长。如果您故意将应用程序设计成只使用4个线程,那么您可能会将自己限制在一个八核系统中(例如)。现在,即使是20核的系统也可以在市场上买到,所以这样的设计肯定弊大于利。