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

时间:2010-06-27 02:18:13

标签: multithreading hardware cpu-cores

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

17 个答案:

答案 0 :(得分:58)

答案围绕线程的目的,即并行性:一次运行几个单独的执行行。在“理想”系统中,每个核心都有一个线程执行:没有中断。实际情况并非如此。即使您有四个核心和四个工作线程,您的进程和它的线程也会不断地被切换为其他进程和线程。如果您正在运行任何现代操作系统,则每个进程至少有一个线程,而且许多进程有更多。所有这些进程都在同时运行。你现在可能在你的机器上运行了几百个线程。你不会遇到线程运行而没有时间“偷”的情况。 (好吧,你可能会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 :(得分:49)

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

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

答案 2 :(得分:24)

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

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

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

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

答案 3 :(得分:10)

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

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

答案 4 :(得分:6)

我强烈不同意@ kyoryu的断言,理想的数字是每个CPU一个线程。

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

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

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

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

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

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

与@kyoryu的分歧只是恕我直言,剩下的就是事实。

答案 5 :(得分:5)

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

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

答案 6 :(得分:5)

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

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

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

答案 7 :(得分:4)

上面的大部分答案都谈到了性能和同步操作。我将从另一个角度来看待这个问题。

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

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

(真正的终端模拟器做得更多,包括可能会回显您在显示器上输入的内容,但我们暂时会将其传递给它。)

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

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绑定流程更具响应性(即使整体效率较低)。它也是关于使逻辑更容易遵循(但只有当你最小化共享状态)。它涉及很多东西,你必须根据具体情况决定它的优点是否超过它的缺点。

答案 8 :(得分:3)

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

答案 9 :(得分:2)

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

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

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

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

答案 10 :(得分:2)

线程可以帮助提高UI应用程序的响应能力。此外,您可以使用线程从核心中获得更多功能。例如,在单个核心上,您可以让一个线程执行IO,另一个执行一些计算。如果它是单线程的,则核心可能基本上处于空闲状态,等待IO完成。这是一个非常高级的例子,但是线程肯定可以用来更加努力地敲击你的cpu。

答案 11 :(得分:1)

一些API的设计方式,你有没有选择,但要在一个单独的线程中运行它们(任何具有阻塞操作的东西)。一个例子是Python的HTTP库(AFAIK)。

通常这不是一个问题(如果它是一个问题,操作系统或API应该附带一个替代的异步操作模式,即:select(2)),因为它可能意味着线程正在运行在等待I / O完成期间睡觉另一方面,如果某些事情正在进行繁重的计算,那么放在一个单独的线程中,而不是GUI线程(除非您喜欢手动多路复用)。

答案 12 :(得分:1)

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

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

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

答案 13 :(得分:0)

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

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

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

答案 14 :(得分:0)

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

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

答案 15 :(得分:-3)

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

答案 16 :(得分:-8)

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

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

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