多线程:线程比核心更重要的是什么?

Nic*_*ner 129 hardware multithreading cpu-cores

我认为多核计算机的意义在于它可以同时运行多个线程.在这种情况下,如果你有一台四核机器,那么一次运行超过4个线程的重点是什么?难道他们不会只是在偷彼此的时间吗?

Dav*_*vid 72

答案围绕线程的目的,即并行性:一次运行几个单独的执行行.在"理想"系统中,每个核心都有一个线程执行:没有中断.实际情况并非如此.即使您有四个核心和四个工作线程,您的进程和它的线程也会不断地被切换为其他进程和线程.如果您正在运行任何现代操作系统,则每个进程至少有一个线程,而且许多进程有更多.所有这些进程都在同时运行.你现在可能在你的机器上运行了几百个线程.你不会遇到线程运行而没有时间"偷"的情况.(好吧,如果它正在运行,你可能会这样,如果您使用的是实时操作系统,或者即使在Windows上,也使用实时线程优先级.但它很少见.)

以此为背景,答案是:是的,真正的四核机器上超过四个线程可能会给你一个"彼此间隔时间"的情况,但前提是每个线程需要100%的CPU.如果一个线程没有100%工作(因为一个UI线程可能不是,或者一个线程做了少量工作或等待其他事情),那么另一个被调度的线程实际上是一个好的情况.

它实际上比这更复杂:

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

  • 一个线程真的需要100%的CPU是很少见的.例如,它使用磁盘或网络I/O的那一刻可能会花费时间等待无用的操作.这是一种非常普遍的情况.

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

所以:窃取时间并不是一件坏事(也不是真的被盗:它是系统应该如何工作的.)根据线程的工作类型编写多线程程序,这可能不是CPU -界.根据分析和测量确定所需的线程数.您可能会发现在任务或作业方面进行思考而不是线程更有用:编写工作对象并将其提供给要运行的池.最后,除非你的程序真的对性能至关重要,否则不要太担心:)

  • +1表示"但仅当每个单独的线程需要100%的CPU"时.这是我没有意识到我正在做的假设. (13认同)

Amb*_*ber 53

仅仅因为存在一个线程并不总是意味着它正在积极运行.线程的许多应用程序涉及一些线程进入睡眠状态,直到它们做某事为止 - 例如,用户输入触发线程唤醒,进行一些处理,然后再回到睡眠状态.

本质上,线程是可以彼此独立操作的单个任务,无需了解另一个任务的进度.除了你有能力同时运行之外,很有可能拥有更多这些; 即使他们有时不得不排在队外排队,他们仍然有用.

  • 说得好."每个CPU一个线程"参数仅适用于CPU绑定代码.异步编程是使用线程的另一个原因. (11认同)

Jus*_*eff 26

关键是,尽管线程数超过核心数时没有获得任何实际加速,但您可以使用线程来解开不必相互依赖的逻辑片段.

即使是一个中等复杂的应用程序,使用单个线程尝试快速完成所有操作也可以使代码的"流"散列.单线程花费大部分时间轮询这个,检查它,根据需要有条件地调用例程,除了一点点细节之外,很难看到任何东西.

将此与您可以将线程专用于任务的情况进行对比,以便在查看任何单个线程时,您可以看到该线程正在执行的操作.例如,一个线程可能阻止等待来自套接字的输入,将流解析为消息,过滤消息,并且当有效消息出现时,将其传递给其他工作线程.工作线程可以处理来自许多其他来源的输入.每个代码的代码都会显示干净,有目的的流程,而不必明确检查没有其他事情要做.

以这种方式对工作进行分区允许应用程序依赖操作系统来安排cpu的下一步操作,因此您不必在应用程序的任何位置对可能阻塞的内容和准备处理的内容进行明确的条件检查.


Ice*_*dor 11

如果线程正在等待资源(例如将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语句的主体使用已经加载到寄存器中的值,处理器可以在评估条件之前执行一个或两个分支.一旦条件返回,处理器将应用相应分支的结果并丢弃另一个分支.在这里执行可能无用的工作可能比切换到不同的线程更好,这可能导致颠簸.

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


Cam*_*Cam 6

虽然您可以使用线程来加速计算,具体取决于您的硬件,但它们的主要用途之一是出于用户友好性原因,一次只能执行多项操作.

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

另请参阅此相关问题:线程的实际用途


fis*_*rds 6

我非常不同意@ kyoryu的断言,理想的数字是每个CPU一个线程.

想一想:为什么我们有多处理操作系统?对于大多数计算机历史记录,几乎所有计算机都有一个CPU.然而从20世纪60年代开始,所有"真正的"计算机都具有多处理(也称为多任务)操作系统.

您运行多个程序,以便一个程序可以运行,而其他程序可以阻止IO等.

让我们讨论关于NT之前的Windows版本是否是多任务的争论.从那以后,每个真正的操作系统都有多任务处理.有些人不会把它暴露给用户,但无论如何都要做,比如收听手机收音机,与GPS芯片交谈,接受鼠标输入等.

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

CPU是一种可怕的浪费,所以有很多东西可以随时使用它.

我同意,对于大多数过程语言,C,C++,Java等,编写适当的线程安全代码是很多工作.目前市场上有6个核心CPU和不远处的16个核心CPU,我希望人们可以摆脱这些旧语言,因为多线程越来越成为一个关键要求.

与@kyoryu的分歧只是恕我直言,其余的事实.

  • 如果你有很多**处理器绑定的**线程,那么理想的数字是每个CPU一个(或者可能少一个,留下一个来管理所有的I/O和OS以及所有这些东西).如果你有*IO-bound*线程,你可以在一个CPU上堆叠很多.不同的应用程序具有处理器绑定和IO绑定任务的不同组合; 这是完全自然的,但为什么你必须小心普遍的声明. (5认同)

小智 5

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

它还使服务器的开发人员更容易:您只需编写一个服务请求的线程程序,您不必考虑存储多个请求,您提供服务的顺序等等.

  • 您是否正在为支持线程但无法复用io的操作系统编写软件?我认为Web服务器可能是一个不好的例子,因为在这种情况下,多路复用io几乎总是比产生更多线程比核心更有效. (2认同)

JUS*_*ION 5

上面的大多数答案都谈到性能和同时运行。我将从另一个角度来解决这个问题。

让我们以一个简单的终端仿真程序为例。您必须执行以下操作:

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

(真正的终端仿真器做得更多,包括潜在地将您键入的内容也回显到显示器上,但是我们暂时将其忽略。)

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

while get-character-from-remote:
    print-to-screen character
Run Code Online (Sandbox Code Playgroud)

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

while get-character-from-keyboard:
    send-to-remote character
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

即使在这个故意简化的示例中也没有考虑到通信的实际复杂性,该逻辑还是很模糊。但是,有了线程,即使在单个内核上,两个伪代码循环也可以独立存在,而无需交织其逻辑。由于这两个线程大部分都是受I / O约束的,因此即使严格地讲,它们比集成循环浪费的CPU资源也更多,但它们不会给CPU带来沉重的负担。

当然,现在实际使用情况比上述情况更为复杂。但是,随着您对应用程序增加更多的关注,集成循环的复杂性将成倍增加。逻辑变得越来越分散,您必须开始使用状态机,协程等技术来使事情易于管理。可管理,但不可读。线程使代码更具可读性。

那为什么不使用线程呢?

好吧,如果您的任务是CPU约束的,而不是I / O约束的,则线程化实际上会降低系统的速度。性能会受到影响。很多情况下。(“丢弃”是一个常见问题,如果您删除过多的CPU绑定线程。您花费更多的时间更改活动线程比运行线程内容本身要花费更多的时间。)而且,上述逻辑的原因之一是如此简单,是我特意选择了一个简单(且不现实)的示例。如果您想在屏幕上回显键入的内容,那么在引入共享资源锁定时,您将遭受新的痛苦。仅拥有一个共享资源,这并不是什么大问题,但是随着您有更多资源可共享,它的确开始变得越来越大。

因此,最后,线程涉及很多事情。例如,正如一些人已经说过的那样,这是关于使I / O绑定的进程更具响应性(即使整体效率较低)。这也是使逻辑更易于遵循(但前提是要最小化共享状态)。它涉及很多东西,您必须根据具体情况确定其优点是否大于缺点。