我认为多核计算机的意义在于它可以同时运行多个线程。在那种情况下,如果你有一台四核机器,那么同时运行 4 个以上的线程有什么意义呢?他们不会只是在互相窃取时间(CPU 资源)吗?
17 回答
答案围绕线程的目的,即并行性:一次运行多个单独的执行行。在一个“理想”系统中,每个内核都会执行一个线程:没有中断。事实上,情况并非如此。即使您有四个内核和四个工作线程,您的进程和它的线程也会不断地被其他进程和线程切换出来。如果您正在运行任何现代操作系统,那么每个进程都至少有一个线程,而且许多线程还有更多。所有这些进程同时运行。你现在可能有几百个线程都在你的机器上运行。您永远不会遇到线程在没有时间“偷走”的情况下运行的情况。(好吧,如果它是实时运行的,你可能会,如果您使用的是实时操作系统,或者甚至在 Windows 上,请使用实时线程优先级。但很少见。)
以此为背景,答案是:是的,真正的四核机器上的四个以上线程可能会给您一种“相互窃取时间”的情况,但前提是每个单独的线程都需要 100% CPU。如果一个线程没有 100% 工作(因为 UI 线程可能没有工作,或者一个线程做少量工作或等待其他事情),那么另一个线程被调度实际上是一个很好的情况。
它实际上比这更复杂:
如果您有五项工作需要同时完成怎么办?一次运行它们比运行其中四个然后再运行第五个更有意义。
很少有线程真正需要 100% CPU。例如,在它使用磁盘或网络 I/O 的那一刻,它可能会花时间等待无所事事。这是很常见的情况。
如果您有需要运行的工作,一种常见的机制是使用线程池。拥有与内核相同数量的线程似乎是有意义的,但.Net 线程池每个处理器最多有 250 个线程可用。我不确定他们为什么这样做,但我的猜测是与线程上运行的任务的大小有关。
所以:偷时间不是一件坏事(也不是真正的偷窃:这是系统应该如何工作的。)根据线程将执行的工作类型编写多线程程序,这可能不是 CPU -边界。根据分析和测量确定您需要的线程数。您可能会发现考虑任务或作业而不是线程更有用:编写工作对象并将它们提供给池以运行。最后,除非您的程序真正对性能至关重要,否则不要太担心:)
仅仅因为一个线程存在并不总是意味着它正在积极运行。许多线程应用程序涉及一些线程进入休眠状态,直到它们需要做某事 - 例如,用户输入触发线程唤醒,进行一些处理,然后重新进入休眠状态。
从本质上讲,线程是可以相互独立运行的单个任务,无需了解另一个任务的进度。很可能拥有比同时运行的能力更多的这些;即使他们有时不得不排成一列,它们仍然可以方便地使用。
关键是,尽管当线程数超过核心数时没有得到任何真正的加速,但您可以使用线程来解开不应相互依赖的逻辑片段。
即使在一个中等复杂的应用程序中,使用单个线程尝试快速完成所有事情都会使代码的“流”散列。单线程大部分时间都在轮询这个,检查那个,根据需要有条件地调用例程,除了一堆细节之外,很难看到任何东西。
将此与您可以将线程专用于任务的情况进行对比,以便查看任何单个线程,您可以看到该线程正在做什么。例如,一个线程可能会阻塞等待来自套接字的输入,将流解析为消息,过滤消息,当出现有效消息时,将其传递给其他工作线程。工作线程可以处理来自许多其他来源的输入。这些代码中的每一个都将展示一个干净、有目的的流程,而无需明确检查是否没有其他事情可做。
以这种方式对工作进行分区允许您的应用程序依赖操作系统来安排接下来对 cpu 执行的操作,因此您不必在应用程序的任何地方都进行明确的条件检查,以了解可能阻塞的内容和准备处理的内容。
如果线程正在等待资源(例如从 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 语句的主体使用已加载到寄存器中的值,则处理器可能会在评估条件之前执行一个或两个分支。一旦条件返回,处理器将应用相应分支的结果并丢弃另一个。在这里执行可能无用的工作可能比切换到不同的线程更好,这可能会导致颠簸。
随着我们从高时钟速度的单核处理器转向多核处理器,芯片设计的重点是在每个裸片上塞进更多的内核,改善内核之间的片上资源共享,更好的分支预测算法,更好的线程切换开销,和更好的线程调度。
上面的大多数答案都在谈论性能和同时操作。我将从不同的角度来解决这个问题。
让我们以一个简单的终端仿真程序为例。您必须执行以下操作:
- 监视来自远程系统的传入字符并显示它们
- 监视来自键盘的东西并将它们发送到远程系统
(真正的终端仿真器做的更多,包括可能将您键入的内容回显到显示器上,但我们现在将忽略它。)
现在从远程读取的循环很简单,根据以下伪代码:
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 绑定的进程更具响应性(即使整体效率较低)。这也是为了让逻辑更容易理解(但前提是你最小化共享状态)。它涉及很多东西,您必须根据具体情况确定其优势是否大于劣势。
尽管您当然可以根据您的硬件使用线程来加速计算,但出于用户友好的原因,它们的主要用途之一是一次做不止一件事。
例如,如果您必须在后台进行一些处理并保持对 UI 输入的响应,您可以使用线程。如果没有线程,每次您尝试进行繁重的处理时,用户界面都会挂起。
另请参阅此相关问题:线程的实际用途
我强烈不同意@kyoryu 的断言,即理想的数字是每个 CPU 一个线程。
这样想:为什么我们有多处理操作系统?在计算机历史的大部分时间里,几乎所有计算机都有一个 CPU。然而,从 1960 年代开始,所有“真正的”计算机都具有多处理(又称多任务)操作系统。
您运行多个程序,以便一个可以运行,而其他程序则因诸如 IO 之类的事情而被阻止。
让我们抛开关于 NT 之前的 Windows 版本是否是多任务处理的争论。从那时起,每个真正的操作系统都有多任务处理。有些人不会把它暴露给用户,但它还是在那里,做一些事情,比如听手机收音机、与 GPS 芯片交谈、接受鼠标输入等。
线程只是更高效的任务。任务、进程和线程之间没有根本区别。
CPU 是一件很浪费的东西,所以尽可能多地准备好使用它。
我同意,对于大多数过程语言,C、C++、Java 等,编写适当的线程安全代码是一项繁重的工作。今天市场上有 6 核 CPU,而 16 核 CPU 也不远了,我希望人们会远离这些旧语言,因为多线程越来越成为一项关键要求。
不同意@kyoryu 只是恕我直言,其余的都是事实。
想象一个必须为任意数量的请求提供服务的 Web 服务器。您必须并行处理请求,否则每个新请求都必须等到所有其他请求都完成(包括通过 Internet 发送响应)。在这种情况下,大多数 Web 服务器的内核数量远少于它们通常服务的请求数量。
它还使服务器的开发人员更容易:您只需要编写一个服务于请求的线程程序,您不必考虑存储多个请求、服务它们的顺序等等。
许多线程将处于休眠状态,等待用户输入、I/O 和其他事件。
线程可以帮助提高 UI 应用程序的响应能力。此外,您可以使用线程从内核中获得更多工作。例如,在单个内核上,您可以让一个线程执行 IO,而另一个线程执行一些计算。如果它是单线程的,那么内核基本上可以空闲等待 IO 完成。这是一个相当高级的示例,但是线程绝对可以用来使您的 cpu 更加困难。
处理器或 CPU 是插入系统的物理芯片。一个处理器可以有多个内核(内核是芯片中能够执行指令的部分)。如果一个内核能够同时执行多个线程(线程是单个指令序列),那么它在操作系统中就可以表现为多个虚拟处理器。
进程是应用程序的另一个名称。通常,进程是相互独立的。如果一个进程死亡,它不会导致另一个进程也死亡。进程可以进行通信,或共享内存或 I/O 等资源。
每个进程都有一个单独的地址空间和堆栈。一个进程可以包含多个线程,每个线程能够同时执行指令。一个进程中的所有线程共享相同的地址空间,但每个线程都有自己的堆栈。
希望通过这些定义和使用这些基础知识的进一步研究将有助于您的理解。
某些 API 的设计方式,您别无选择,只能在单独的线程中运行它们(任何具有阻塞操作的线程)。一个例子是 Python 的 HTTP 库 (AFAIK)。
不过通常这不是什么大问题(如果有问题,操作系统或 API 应该附带一个替代的异步操作模式,即:)select(2)
,因为这可能意味着线程在等待 I/ 期间将处于休眠状态。 O完成。另一方面,如果某些东西正在执行繁重的计算,则必须将其放在单独的线程中,而不是 GUI 线程中(除非您喜欢手动多路复用)。
实际上,线程的理想用法是每个内核一个。
但是,除非您专门使用异步/非阻塞 IO,否则您很有可能会在某个时候在 IO 上阻塞线程,这不会使用您的 CPU。
此外,典型的编程语言使得每个 CPU 使用 1 个线程有些困难。围绕并发设计的语言(例如 Erlang)可以更容易地不使用额外的线程。
回应你的第一个猜想:多核机器可以同时运行多个进程,而不仅仅是单个进程的多个线程。
回答您的第一个问题:多线程的意义通常是在一个应用程序中同时执行多个任务。网络上的经典例子是一个发送和接收邮件的电子邮件程序,以及一个接收和发送页面请求的网络服务器。(请注意,基本上不可能将像 Windows 这样的系统减少到只运行一个线程甚至只运行一个进程。运行 Windows 任务管理器,您通常会看到一长串活动进程,其中许多将运行多个线程。 )
回答您的第二个问题:大多数进程/线程不受 CPU 限制(即,不是连续运行且不间断),而是经常停止并等待 I/O 完成。在该等待期间,其他进程/线程可以在不从等待代码“窃取”的情况下运行(即使在单核机器上)。
我知道这是一个非常古老的问题,有很多很好的答案,但我在这里指出在当前环境中很重要的一些事情:
如果您想为多线程设计应用程序,则不应针对特定的硬件设置进行设计。多年来,CPU 技术一直在快速发展,核心数量也在稳步增加。如果您故意将应用程序设计为仅使用 4 个线程,那么您可能会将自己限制在八核系统中(例如)。现在,即使是 20 核系统也已经商用,所以这样的设计肯定是弊大于利。
线程是一种抽象,它使您可以编写像一系列操作一样简单的代码,完全不知道代码与其他代码交错执行,或者等待 IO,或者(可能更清楚)等待其他线程的事件或消息。
关键是绝大多数程序员不了解如何设计状态机。能够将所有内容放在自己的线程中,程序员就不必考虑如何有效地表示不同正在进行的计算的状态,以便它们可以被中断并在以后恢复。
例如,考虑视频压缩,这是一项非常占用 CPU 资源的任务。如果您使用的是 gui 工具,您可能希望界面保持响应(显示进度、响应取消请求、调整窗口大小等)。因此,您将编码器软件设计为一次处理一个大型单元(一个或多个帧),并在其自己的线程中运行它,与 UI 分开。
当然,一旦您意识到能够保存正在进行的编码状态以便您可以关闭程序以重新启动或玩一个资源密集型游戏会很好,您就会意识到您应该从开始。要么,要么你决定设计一个全新的进程休眠问题,你的操作系统,这样你就可以暂停和恢复单个应用程序到磁盘......