为什么通常首选多线程来提高性能?


23

我有一个问题,那就是为什么程序员似乎总体上喜欢并发和多线程程序。

我在这里考虑2种主要方法:

  • 一种基本上基于信号的异步方法,或者只是许多论文和语言(例如,新的C#5.0)所调用的一种异步方法,以及一个用于管理管道策略的“伴侣线程”
  • 并发方法或多线程方法

我只是说,我正在考虑这里的硬件和最坏的情况,并且我自己已经测试了这两种范例,异步范例是赢家,因为我不明白为什么人们有90%的情况当他们想要加快工作速度或充分利用他们的资源时,谈论多线程。

我已经在一台旧计算机上使用Intel四核测试了多线程程序和异步程序,该英特尔四核在CPU内不提供内存控制器,内存完全由主板管理,在这种情况下,性能令人震惊。多线程应用程序,即使是数量相对较少的线程(例如3-4-5)也可能是一个问题,该应用程序没有响应,并且运行缓慢且令人不快。

另一方面,一种好的异步方法可能不会更快,但也并不坏,我的应用程序只是等待结果并且不会挂起,它具有响应能力,并且伸缩性更好。

我还发现,线程世界中的上下文更改在现实世界中并不是那么便宜,实际上它相当昂贵,尤其是当您有两个以上的线程需要循环和相互交换来计算时。

在现代CPU上,集成的内存控制器的情况并没有什么不同,但我的观点是x86 CPU基本上是一台串行计算机,并且该内存控制器的工作方式与在主板上带有外部内存控制器的旧计算机的工作方式相同。 。上下文切换仍然是我应用程序中的一个相关成本,并且集成了内存控制器或较新的CPU具有2个以上内核的事实对我来说并不便宜。

因为我所经历的并发方法在理论上是好的,但在实践中却不是那么好,由于硬件强加了内存模型,很难很好地利用这种范式,而且还引入了很多问题,从使用到使用我的数据结构连接到多个线程。

同样,这两种范式都不能提供任何安全性,而只是在某个时间点完成任务或工作时,从功能的角度来看,它们确实非常相似。

根据X86内存模型,为什么大多数人建议对C ++使用并发,而不仅仅是异步方法?另外,为什么不考虑环境切换可能比计算本身更昂贵的计算机的最坏情况呢?


2
比较的一种方法是查看JavaScript的世界,如果没有线程并且一切都使用回调来实现异步。它可以工作,但是有它自己的问题。

2
@StevenBurnap您怎么称呼网络工作者?
user16764

2
“即使是数量相对较少的线程(例如3-4-5)也可能是一个问题,该应用程序没有响应,并且运行缓慢且令人不快。” =>可能是由于设计不良/线程使用不当所致。当线程继续交换数据时,通常会遇到这种情况,在这种情况下,多线程可能不是正确的答案,或者您可能需要重新分区数据。
assylias 2012年

1
@assylias要查看UI线程的显着下降,则表明线程之间的锁定量过多。您要么执行效果不佳,要么试图将方形钉敲入圆孔。
伊万·普赖斯

5
您说“程序员似乎总体上喜欢并发和多线程程序”,我对此表示怀疑。我会说“程序员讨厌它”……但是通常这是唯一有用的事情……
johannes 2012年

Answers:


34

您有多个核心/处理器,请使用它们

异步最好的做沉重的IO约束处理但对于大量CPU的约束处理?

当单线程代码块(即卡住)在长时间运行的进程上时,就会出现问题。例如,还记得在打印文字处理器文档时会冻结整个应用程序直到发送作业吗?应用程序冻结是CPU密集型任务期间单线程应用程序阻塞的副作用。

在多线程应用程序中,可以将CPU密集型任务(例如打印作业)发送到后台工作线程,从而释放UI线程。

同样,在多进程应用程序中,可以通过消息传递(例如IPC,套接字等)将作业发送到专门用于处理作业的子进程。

实际上,异步多线程/进程代码各有其优缺点。

您可以看到主要云平台的趋势,因为它们将提供专门用于CPU绑定处理的实例和专门用于IO绑定处理的实例。

例子:

  • 存储(例如Amazon S3,Google Cloud Drive)受CPU限制
  • Web服务器受IO约束(Amazon EC2,Google App Engine)
  • 两者都是数据库,CPU限制用于写/索引,而IO限制用于读取

透视一下...

Web服务器是受IO严格约束的平台的完美示例。为每个连接分配一个线程的多线程Web服务器无法很好地扩展,因为由于上下文切换和共享资源上的线程锁定的增加,每个线程都会产生更多开销。异步Web服务器将使用单个地址空间。

同样,专门用于视频编码的应用程序在多线程环境中会更好地工作,因为涉及的繁重处理将锁定主线程,直到完成工作为止。有多种方法可以减轻这种情况,但是由一个线程管理队列,由第二个线程管理清除以及由线程池管理繁重的处理要容易得多。线程之间的通信仅在任务分配/完成时发生,因此线程锁定开销保持在最低限度。

最好的应用程序经常将两者结合使用。例如,一个Web应用程序可以使用Nginx(即异步单线程)作为负载平衡器来管理传入请求的洪流,一个类似的异步Web服务器(例如Node.js)来处理http请求,以及一组多线程服务器。处理上传/流/编码内容等...

多年来,在多线程,多进程和异步模型之间发生了许多宗教战争。与大多数情况一样,最佳答案实际上应该是“取决于情况”。

它遵循相同的思路,可以并行使用GPU和CPU架构。与单一的整体方法相比,两个协同运行的专用系统可以带来更大的改进。

两者都没有用,因为两者都有用。使用最好的工具完成工作。

更新:

我删除了对Apache的引用,并做了一些小的更正。Apache使用多进程模型,该模型为每个请求派生一个进程,从而增加了内核级别的上下文切换量。此外,由于无法跨进程共享内存,因此每个请求都会产生额外的内存成本。

多线程解决方案需要额外的内存,因为它依赖于线程之间的共享内存。共享内存消除了额外的内存开销,但仍然带来上下文切换增加的代价。另外,为了确保不发生竞争条件,跨线程共享的任何资源都需要线程锁(确保一次仅对一个线程的独占访问)。

有趣的是,您说:“程序员似乎总喜欢并发和多线程程序。” 任何时候都花了大量时间进行编程的人都普遍害怕多线程编程。死锁和(当这种情况发生的资源被错误地由两个不同的来源,从以往整理阻塞都会锁定一个bug)竞态条件(程序会误输出错误的结果随机由于不正确的测序)是一些最难以跟踪下来并修复。

更新2:

与关于IPC比网络(即套接字)通信更快的笼统声明相反。并非总是如此。请记住,这些只是概括,特定于实现的细节可能会对结果产生巨大影响。


为什么程序员应该去多进程?我的意思是我假设,使用多个进程,您还需要某种进程间通信,这可能会增加大量开销,这是否类似于旧的Windows程序员的工作方式?我什么时候应该进行多进程处理?顺便说一句,感谢您的答复,关于异步和多线程的优势非常不错。
user1849534

1
您假设进程间通信会增加总体开销。但是,如果处理状态是不可变的,或者仅需要在启动/完成时处理同步。展开更多并行任务可能会更加高效。演员模式是一个很好的例子,如果您还没有阅读过它,那么确实值得继续阅读。akka.io
sylvanaar 2012年

1
@ user1849534多个线程可以通过共享内存+锁定 IPC相互通信。如果您犯了错误(例如未锁,死锁),则锁更容易但更难调试。如果您有很多工作线程,则IPC是最佳选择,因为锁定无法很好地扩展。无论哪种方式,如果您使用的是多线程方法,则将线程之间的通信/同步保持在绝对最小(即使开销最小化)非常重要。
伊万·普赖斯

1
@ akka.io你是完全正确的。不变性是最小化/消除锁定开销的一种方法,但是您仍然要花费上下文切换的时间成本。如果您想扩展答案以包括有关不变性如何解决线程同步问题的详细信息,请放心。我要说明的重点是,在某些情况下,异步通信比多线程/进程具有明显的优势,反之亦然。
Evan Plaice 2012年

(续)但是,老实说,如果我需要大量的CPU绑定处理能力,我将跳过参与者模型,并将其构建为能够扩展到多个网络节点的模型。我看到的最好的解决方案是在套接字级别的通信中使用0MQ的任务呼吸机模型。参见图5 @ zguide.zeromq.org/page:all
伊万·普赖斯

13

对于多线程编程的最常见目的,Microsoft的异步方法是很好的替代品:提高对IO任务的响应能力。

但是,重要的是要意识到异步方法根本无法提高性能,也无法提高对CPU密集型任务的响应能力。

多线程响应

用于响应性的多线程是使程序在繁重的IO任务或繁重的计算任务期间保持响应能力的传统方法。您可以将文件保存在后台线程中,以便用户可以继续工作,而不必等待硬盘驱动器完成其任务。IO线程通常会阻塞等待写入的某些部分完成,因此上下文切换非常频繁。

同样,执行复杂的计算时,您希望允许定期进行上下文切换,以便UI可以保持响应状态,并且用户不会认为程序已崩溃。

通常,这里的目标不是让多个线程在不同的CPU上运行。相反,我们只是想让上下文切换在长时间运行的后台任务和UI之间发生,以便UI能够在后台任务运行时更新并响应用户。通常,UI不会占用太多CPU功能,并且线程框架或操作系统通常会决定在同一CPU上运行它们。

实际上,由于上​​下文切换的额外成本,我们实际上失去了整体性能,但是我们不在乎,因为CPU的性能并不是我们的目标。我们知道通常我们的CPU能力超出了需要,因此我们在多线程方面的目标是在不浪费用户时间的情况下为用户完成任务。

“异步”替代

“异步方法”通过在单个线程中启用上下文切换来更改此情况。这样可以保证我们所有的任务都可以在单个CPU上运行,并且可以在较少的线程创建/清理和较少的线程之间实际上下文切换方面提供适度的性能改进。

代替创建新线程来等待网络资源的接收(例如,下载图像),使用一种async方法,该方法await使图像可用,并且同时产生调用方法。

这里的主要优点是您不必担心避免死锁之类的线程问题,因为您根本不使用锁和同步,并且程序员设置后台线程并返回的工作量少了一些当结果返回时,在UI线程上单击,以便安全地更新UI。

我还没有深入研究技术细节,但是我的印象是,偶尔进行少量CPU活动来管理下载不是一项针对单独线程的任务,而是一项类似于UI事件队列中的任务,以及下载完成后,将从该事件队列中恢复异步方法。换句话说,await意味着类似于“检查所需的结果是否可用,如果没有,将我放回到该线程的任务队列中”。

请注意,这种方法无法解决CPU密集型任务的问题:没有等待数据,因此,如果不创建实际的后台工作线程,就无法获得需要进行的上下文切换。当然,在普遍使用异步方法的程序中,使用异步方法启动后台线程并返回结果可能仍然很方便。

多线程性能

自从您谈论“性能”以来,我还想讨论如何将多线程用于提高性能,这对于单线程异步方法是完全不可能的。

当您实际上处于一个CPU上没有足够的CPU能力并且想要使用多线程来提高性能的情况下,实际上通常很难做到。另一方面,如果一个CPU的处理能力不足,它通常也是使程序能够在合理的时间范围内完成您想完成的工作的唯一解决方案,这使工作值得。

琐碎的平行主义

当然,有时候从多线程获得真正的加速可能很容易。

如果您碰巧有大量独立的计算密集型任务(即,对于确定结果必须执行的计算而言,其输入和输出数据很小的任务),则通常可以通过以下方式显着提高速度:创建一个线程池(根据可用CPU的数量适当确定大小),并让主线程分发工作并收集结果。

实用的多线程性能

我不想让自己成为专家,但我的印象是,通常来说,最近出现的最实用的多线程性能正在寻找应用程序中琐碎的并行性并使用多个线程收获好处。

与任何优化一样,通常最好在分析程序的性能并确定热点后进行优化:通过任意决定该部分应在一个线程中运行而该部分在另一个线程中运行,很容易减慢程序速度,首先确定这两个部分是否都占用了大量CPU时间。

额外的线程意味着更多的设置/拆卸成本,或者更多的上下文切换或更多的CPU间通信成本。如果在单独的CPU上没有做足够的工作来弥补这些成本,并且由于响应性原因而不必成为单独的线程,那么它将使速度变慢而没有任何好处。

寻找没有相互依赖性的任务,这些任务占用程序运行时的很大一部分。

如果它们之间没有相互依赖性,那么这就是微不足道的并行性,您可以轻松地为每个线程设置线程并享受其中的好处。

如果可以找到相互依存性有限的任务,从而使锁定和同步来交换信息不会显着降低它们的速度,那么多线程可以加快速度,前提是您要小心避免由于同步或同步时逻辑错误而导致死锁的危险。由于在必要时未同步,导致错误结果。

或者,某些更常见的多线程应用程序不是(某种意义上)不是在寻求预定算法的加速,而是为了他们计划编写的算法有更大的预算:如果您正在编写游戏引擎,并且您的AI必须在您的帧频范围内做出决定,如果您可以给自己的CPU分配预算,则通常可以给它更大的CPU周期预算。

但是,请确保对线程进行概要分析,并确保它们做了足够的工作以弥补某些时候的成本。

并行算法

使用多个处理器可以解决很多问题,但是这些问题过于单一,无法简单地在多个CPU之间分配。

相对于最佳可用非并行算法,并行算法的big-O运行时必须仔细分析,因为CPU间的通信成本很容易消除使用多个CPU带来的任何好处。通常,与在每个CPU上使用计算相比,它们使用的CPU间通信(以big-O表示)必须更少。

目前,它仍然是学术研究的主要空间,部分原因是需要进行复杂的分析,部分原因是琐碎的并行性非常普遍,部分原因是我们的计算机上还没有那么多的CPU内核,因此会出现问题。无法在一个合理的时间范围内解决一个CPU可能在一个合理的时间范围内使用我们所有的CPU解决。


+1是一个经过深思熟虑的答案。不过,我还是要谨慎对待微软的建议。请记住,.NET是一个同步优先平台,因此生态系统倾向于提供更好的功能/文档来支持构建同步解决方案。对于像Node.js这样的异步优先平台,情况恰恰相反。
Evan Plaice 2015年

3

该应用程序无响应,并且运行缓慢且令人不快。

还有你的问题。响应式UI不会成为高性能应用程序。通常是相反的。花大量时间检查UI输入,而不是让工作线程完成工作。

至于“仅”具有异步方法,尽管在大多数环境中针对一个特定用例进行了调整,但它也是多线程的。在其他情况下,异步是通过协程完成的,协程并不总是并发的。

坦白说,我发现异步操作更难以推理和使用,即使与...更手动的方法相比,它实际上也可以提供好处(性能,健壮性,可维护性)。


为什么呢?例如,您在boost signal2库中找到了香蕉吗?
user1849534
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.