虽然线程可以加快代码执行速度,但实际上是否需要它们?可以使用单个线程来完成每段代码,还是存在只能通过使用多个线程来完成的事情?

评论

根据已经在此处发布的一些答案,我认为需要进行一些澄清。线程化的目的是允许计算机一次(看起来)做一件事情。如果使用具有单核的计算机完成此操作,则不会看到任何整体加速。您将看到的是畅通无阻;在单线程程序中,在进行计算时,用户界面将阻塞。在多线程程序中,在后台进行计算时,用户界面可能仍然可用。

当然,如果您的计算机中确实有多个处理器内核(当今很常见),则您的程序可以使用线程来利用其他内核来执行您的处理,因此您会看到速度提高了,因为您将获得更多的处理能力您的计算问题。如果您根本不使用线程,则您的程序将仅使用单个处理器核心,如果您需要的话,这很好。

单线程程序无法像多线程程序那样死锁资源。

答:在两个内核上运行

答:造成死锁。

#1 楼

首先,线程无法加快代码执行速度。它们不会使计算机运行得更快。他们所能做的就是利用可能浪费的时间来提高计算机的效率。在某些类型的处理中,这种优化可以提高效率并减少运行时间。

简单的答案是肯定的。您可以编写要在单个线程上运行的任何代码。证明:单处理器系统只能线性运行指令。通过操作系统处理中断,保存当前线程的状态并启动另一个线程来完成多行执行。

复杂的答案是……更加复杂!多线程程序通常可能比线性程序更有效的原因是由于硬件“问题”。与内存和硬存储IO相比,CPU可以更快地执行计算。因此,例如,“添加”指令的执行速度远快于“获取”指令。高速缓存和专用程序指令获取(此处不确定确切的术语)可以在某种程度上解决此问题,但是速度问题仍然存在。

线程化是通过使用CPU来解决CPU不匹配问题的一种方法。 IO指令完成时绑定指令。典型的线程执行计划可能是:获取数据,处理数据,写入数据。出于说明的目的,假设读取和写入需要3个周期,而处理只需要1个周期。您会看到计算机正在读取或写入时,每台计算机都没有执行2个周期的操作吗?显然这很懒,我们需要破解我们的优化工具!

我们可以使用线程来重写该过程以使用浪费的时间:


#1获取
无操作
#2提取
#1完成,对其进行处理
写入#1
#1提取
#2完成,对其进行处理
写入#2
获取#2

等等。显然,这是一个有些人为的示例,但是您可以看到该技术如何利用原本会花费在等待IO上的时间。

请注意,如上所示的线程处理只能在IO受限的情况下提高效率流程。如果程序主要是在计算东西,那么就不会有很多“漏洞”,我们可以做更多的工作。而且,在线程之间进行切换时,会有多条指令的开销。如果运行的线程过多,则CPU将花费大部分时间在切换上,而实际上没有多少时间在解决该问题上。这称为抖动。

对于单核处理器来说一切都很好,但是大多数现代处理器具有两个或更多核。线程仍然具有相同的目的-最大限度地利用CPU,但是这次我们可以同时运行两条单独的指令。由于计算机实际上是多任务的,而不是上下文切换,因此可以将运行时间减少多少倍,因为可用的内核很多。

使用多核,线程提供了一种在两个内核之间分配工作的方法。以上内容仍然适用于每个核心;一个在一个内核上有两个线程的情况下运行最高效率的程序最有可能在两个内核上有四个线程的情况下以峰值效率运行。 (这里的效率是通过最少的NOP指令执行来衡量的。)

多核(而不是单核)上运行线程的问题通常由硬件解决。 CPU将确保在对其进行读/写之前锁定适当的存储器位置。 (我已经读到它为此在内存中使用了一个特殊的标志位,但是可以通过几种方式来完成。)作为使用高级语言的程序员,您不必担心两个内核上的任何事情将不得不与一个。

TL; DR:线程可以拆分工作,以允许计算机异步处理多个任务。这使计算机可以利用所有可用的处理时间以最高的效率运行,而不是在进程等待资源时锁定。

评论


极好的答案。我只是想说明一点,在特定点之后,添加更多的线程实际上会使程序运行更慢,因为以前所有的“浪费”时间都被利用了,而现在您仅添加了额外的上下文切换和同步开销。

–卡尔·比勒费尔特(Karl Bielefeldt)
2011年8月1日15:55

+1描述线程如何帮助IO阻止处理。线程的另一种用法是允许UI继续处理用户动作(鼠标移动,进度条,击键),因此用户认为程序没有“冻结”。

– jqa
2011年8月1日在16:04

还有一个补充:严格来说,线程不会对大多数语言增加任何表达;如果绝对必须,您可以自己实现所有多路复用并获得相同的结果。但是,实际上,这相当于重新实现整个线程库-就像并非严格要求递归一样,但是可以通过编写自己的调用堆栈来模拟。

–基连·福斯(Kilian Foth)
2011年8月1日在16:59

@james:我会将UI处理归类为另一种I / O形式。可能是最坏的情况,因为用户往往比较懒惰和非线性。 :)

– TMN
2011年8月1日在18:31

关于计算机是单核还是CPU,在问答中有一个默默的假设。我的手表有两个核心,我怀疑这种假设在任何现代机器上都是正确的。

–蓝莓田
2011年8月1日在18:38

#2 楼


多个线程能做一个线程不能做的事情?


没什么。

简单的证明草图:


[Church-Turing Conjecture]⇒可以计算的所有内容都可以由通用图灵机计算。
通用图灵机是单线程的。
Ergo,可以计算的一切都可以

,但是,这里隐藏着一个很大的假设:即,单线程内使用的语言是图灵完备的。

因此,更有趣的问题是:“可以在非图灵完备的语言中仅添加多线程就可以使其成为图灵完备的语言吗?”而且我相信答案是“是”。

让我们采用全面功能语言。 [对于不熟悉的人:就像函数式编程就是使用函数式编程一样,总函数式编程就是利用函数式编程。]

总函数式语言显然不是图灵完备的:您不能编写无限在TFPL中循环(实际上,这几乎是“总计”的定义),但是您可以在Turing Machine中,因此至少存在一个不能在TFPL中编写但可以在UTM中编写的程序,因此TFPL是

但是,一旦在TFPL中添加线程,就会出现无限循环:只需在新线程中进行循环的每次迭代即可。每个单独的线程总是返回结果,因此它是Total,但是每个线程也产生一个新的线程来无限执行下一次迭代。

我认为这种语言是图灵完备的。 br />
至少可以回答原始问题:


多个线程可以做一个线程不能做的事情?


如果您的语言无法执行无限循环,那么多线程允许您进行无限循环。

请注意,当然,产生一个线程是一种副作用,因此我们的扩展语言不仅不再是Total语言,甚至不再是Functional语言。

评论


由于您提出了“证明”,因此我无法阻止自己“嗯,实际上……”对您的伤害。我认为您正在滥用可计算性的概念。证明是一件很可爱的事,我明白您的意思,但这并不是真正的正确抽象层。无法执行操作与无法执行计算是不一样的,例如,您不能使用证明来证明,因为图灵机可以计算“每个可计算函数”,所以它可以在3秒钟内完成。严格来说,没有中断的单线程计算机无法访问I / O,同时使处理器忙于进行计算。

–mmx
2011年8月1日23:14

而且,没有真正的计算机是图灵机。

–詹斯
2011年8月2日在7:14

@ColeJohnson:死锁是实现细节。可见的输出是“不停止”,这很容易用一个线程完成。

–亨氏
2013年9月12日上午10:47

@Heinzi:“容易实现”是一种轻描淡写的说法。如果我没记错的话,我曾经写过的第二个或第三个程序就做到了! ;-)

– Joachim Sauer
2013年9月12日上午10:57

如果宇宙是单线程的,那么弦论是什么?你不能与这样的科学争论。

– corsiKa
14年6月17日在17:13

#3 楼

从理论上讲,多线程程序所做的所有事情都可以用单线程程序完成,只是速度较慢。

实践中,速度差异可能很大,无法使用单个线程。该任务的线程程序。例如。如果您有一个每晚运行的批处理数据处理作业,并且在一个线程上完成要花费24个小时以上,那么除了将其设置为多线程之外,您别无选择。 (在实践中,阈值可能更低:通常,此类更新任务必须在用户再次开始使用系统之前的清晨完成。此外,其他任务可能取决于它们,这些任务也必须在同一晚完成。

在多个线程上执行计算工作是分布式处理的一种形式;您将工作分配到多个线程上。分布式处理(使用多台计算机而不是多个线程)的另一个示例是SETI屏幕保护程序:在单个处理器上处理大量测量数据将花费很长时间,而且研究人员更希望在退休之前先查看结果;-)但是,他们太久没有租用超级计算机的预算,因此他们将工作分配到数百万台家用PC上,以使其便宜。

评论


SETI屏幕保护程序在您的计算机的后台线程上运行时就是一个例子。当计算机在后台处理数据时,您仍然可以在计算机上执行其他操作。没有线程,SETI屏幕保护程序执行计算时将暂停。您必须先等待它完成,然后才能继续自己的工作。

–罗伯特·哈维(Robert Harvey)
2011年8月1日15:42



@Robert:我认为Péter正在使用SETI @ Home来解释将工作拆分为在多个处理器上运行的概念。分布式计算当然不同于多线程,但是概念相似。

–史蒂文
2011年8月1日15:49

我认为SETI示例是分布式计算,而不是多线程的。类似的概念,但不是同一件事。

–user7007
2011年8月1日15:49

#4 楼


尽管线程似乎是顺序计算的一小步,但实际上,它们代表了巨大的一步。他们放弃了顺序计算最重要的,最吸引人的属性:可理解性,可预测性和确定性。线程是计算的模型,它是不确定性的,程序员的工作变成了修剪不确定性的事情。

-线程问题(www。 eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-1.pdf)。


虽然使用线程可以带来一些性能优势,但是您可以在多个内核之间分配工作通常要付出很大的代价。

使用此处未提及的线程的缺点之一是,单线程进程空间会浪费资源分隔。例如,假设您遇到了段错误。在某些情况下,可以在多进程应用程序中从中恢复,因为您只需让出现故障的子进程死亡并重新生成一个新的子进程即可。在Apache的prefork后端中就是这种情况。当一个httpd实例崩溃时,最糟糕的情况是可能会为该进程删除特定的HTTP请求,但是Apache会产生一个新的子对象,并且通常会在重新发送和处理该请求时产生该请求。最终结果是整个Apache都没有被错误的线程删除。

在这种情况下的另一个考虑因素是内存泄漏。在某些情况下,您可以适当地处理线程崩溃(在UNIX上,可以从某些特定信号中进行恢复-甚至可能发生segfault / fpviolation),但是即使在那种情况下,您也可能泄漏了该线程分配的所有内存(malloc,new等)。因此,尽管您的过程可能继续存在,但随着时间的推移,每次故障/恢复都会泄漏越来越多的内存。同样,在某种程度上有一些方法可以最小化这种情况,例如Apache对内存池的使用。但是,这仍然不能防止线程可能一直在使用的第三方库分配的内存。

,正如某些人指出的那样,理解同步原语可能是最难的事情真正做到正确。这个问题本身-仅使通用逻辑适用于所有代码-可能会令人头疼。神秘的死锁很容易在最奇怪的时间发生,有时甚至直到您的程序在生产中运行后才发生,这使得调试更加困难。除此之外,同步原语通常会因平台而异(Windows与POSIX),并且调试通常会更加困难,而且随时可能出现竞争状况(启动/初始化,运行时和关闭),对于初学者来说,使用线程编程确实没有什么可怜的。甚至对于专家来说,也几乎没有什么可怜的余地,这仅仅是因为线程本身的知识并不能使复杂性降到最低。有时,每行线程代码似乎都成倍地增加了程序的整体复杂性,并增加了随时出现隐性死锁或奇怪竞争条件的可能性。编写测试用例以将这些内容隐式化也可能非常困难。

这就是为什么某些项目(例如Apache和PostgreSQL)大部分基于流程的原因。 PostgreSQL在单独的进程中运行每个后端线程。当然,这仍然不能减轻同步和竞争条件的问题,但是确实增加了很多保护,并且在某种程度上简化了事情。

多个进程每个运行一个执行线程都可以比在单个进程中运行的多个线程好得多。随着许多新的对等代码(例如AMQP(RabbitMQ,Qpid等)和ZeroMQ)的出现,将线程拆分到不同的进程空间甚至机器和网络上变得更加容易,从而大大简化了工作。但是,这并不是万灵丹。仍然需要处理复杂性。您只需将一些变量从进程空间移到网络中即可。

最重要的是,进入线程域的决定并不轻松。一旦踏入这一领域,几乎所有事情都将变得更加复杂,而全新的问题也将进入您的生活。它既有趣又酷,但是就像核能一样-当发生问题时,它们可能会迅速恶化。我记得很多年前参加过一次临界培训班,他们展示了洛斯阿拉莫斯一些科学家的照片,这些科学家在二战期间曾在实验室里玩过p。许多人很少或根本没有采取预防措施来预防暴晒,眨眼之间-一次明亮,无痛的闪光,对他们来说就完蛋了。几天后,他们死了。理查德·费曼(Richard Feynman)后来将此称为“挠尾巴”。这就是玩线程的样子(至少对我而言)。乍一看似乎是无害的,但是当您被咬时,您就挠了一下头,发现事情变酸了。但是至少线程不会杀死您。

评论


与这个不错的文章相比,我的TL; DR回答显得苍白无力。

–bmike
2011年8月1日19:59

下午喝咖啡休息时间

–迈克·欧文斯(Mike Owens)
2011年8月2日在15:52

#5 楼

首先,单线程应用程序将永远不会利用多核CPU或超线程。但是,即使在单核上,执行多线程的单线程CPU也具有优势。

考虑替代方法以及是否使您满意。假设您有多个任务需要同时运行。例如,您必须保持与两个不同系统的通信。没有多线程怎么办?您可能会创建自己的调度程序,并使其调用需要执行的不同任务。这意味着您需要将任务分成几部分。您可能需要满足一些实时约束,必须确保您的零件不会占用太多时间。否则计时器将在其他任务中过期。这使得拆分任务更加困难。您需要管理的任务越多,需要进行的拆分就越多,满足所有约束的调度程序也将变得更加复杂。

拥有多个线程时,生活会变得更加轻松。抢先式调度程序可以随时停止线程,保持其状态,然后重新(启动)另一个线程。线程轮到时它将重新启动。优点:编写调度程序的复杂性已经为您完成,您不必拆分任务。而且,调度程序能够管理您自己甚至不知道的进程/线程。而且,当线程不需要执行任何操作(正在等待某个事件)时,它将不占用任何CPU周期。创建向下的单线程调度程序时,要完成此任务并不容易。 (让某些东西睡觉并不难,但是怎么唤醒呢?)

多线程开发的缺点是您需要了解并发问题,锁定策略等。开发无错误的多线程代码可能非常困难。而且调试会更加困难。

#6 楼


是否存在只能通过使用多个线程才能完成的事情?


是的。您不能使用单个线程在多个CPU或CPU内核上运行代码。

没有多个CPU /内核,线程仍可以简化概念上并行运行的代码,例如服务器上的客户端处理- -但是您可以在没有线程的情况下执行相同的操作。

#7 楼

线程不仅与速度有关,而且与并发性有关。

如果您没有@Peter建议的批处理应用程序,而是像WPF这样的GUI工具包,如何仅用一个线程即可与用户和业务逻辑进行交互?

另外,假设您正在构建Web服务器。您如何只用一个线程(假设没有其他进程)同时服务多个用户?

在许多情况下,仅一个线程是不够的。这就是为什么最近出现的进步,例如具有50多个内核和数百个线程的Intel MIC处理器。

是的,并行和并发编程很难。但有必要。

评论


“仅一个线程”可能意味着几件事-例如,您可以使用定界的延续来模拟单个(OS或绿色)线程内的协作式多任务处理。

–坦率的剪毛
2011年8月1日15:10

可以,但是我将为@Frank +1问题提供一些背景信息。

–兰道夫·林孔·法杜尔(RandolfRincónFadul)
2011年8月1日15:11



线程可以使它变得更容易,但是您提到的所有任务都可以在没有线程的情况下完成,并且过去确实如此。例如,规范的GUI循环曾经是while(true){处理GUI事件;计算一下}。不需要线程。

– KeithB
2011年8月1日15:22

有单进程单线程Web服务器。例如lighttpd或thttpd或nginx(尽管后者可以运行多个进程,但默认值为1。它始终是单线程的)。他们绝对可以为一个以上的用户提供服务。

– StasM
2011年8月2日,0:34

#8 楼

多线程可以使GUI界面在长时间的处理操作中仍然能够响应。如果没有多线程,则长时间运行过程中,用户将无法观看锁定的表单。

评论


这并不完全正确。从理论上讲,您可以将您的长时间操作分成许多较小的操作,并在两者之间更新事件处理。

–塞巴斯蒂安·内格拉苏斯(Sebastian Negraszus)
11年8月13日在17:57

@Sebastion N.但是,如果不能将漫长的过程重构为更小的过程呢?在另一个线程中运行该进程的能力可以释放GUI线程(主线程)以保持响应。

– LarsTech
11年8月13日在18:05

#9 楼

多线程代码可以使程序逻辑死锁,并以单线程无法实现的方式访问陈旧数据。

线程可以消除一些普通程序员可以调试的东西中的晦涩错误,并将其转移到领域中,从而使人们知道当警报发出警报时需要抓住它的麻烦程序员恰好在适当的时机。

#10 楼

无法处理还需要保持对其他输入(GUI或其他连接)响应的阻塞IO的应用程序,不能使其成为单线程

在IO库中添加检查方法以查看无需读取即可读取多少内容阻塞可以帮助解决此问题,但没有多少图书馆对此提供任何完全保证

评论


它们可以是单线程的(并且经常是)。该代码告诉内核启动一些IO,并在完成时获取信号。在等待信号时,它可以执行其他处理。

– KeithB
2011年8月1日15:24

@keith这是非阻塞IO,我专门说过阻塞

–棘轮怪胎
2011年8月1日15:36



但是,如果您想保持响应状态,为什么还要阻止IO?是否有任何阻塞IO通常没有非阻塞替代方案?我想不到。

– KeithB
2011年8月1日15:50

@keith java的RMI正在阻止,虽然您可以使用它实现回调(但会创建另一个线程),但是可以被防火墙阻止

–棘轮怪胎
2011年8月1日在16:05

这听起来像是Java RMI的限制,而不是单线程代码的固有限制。此外,如果您正在执行RMI,则仅在远程计算机上就有单独的执行“线程”。

– KeithB
2011年8月1日在20:36

#11 楼

有很多不错的答案,但我不确定是否有任何短语-也许这提供了另一种看待方式: for循环(是的,任何使用循环实现的内容都可以使用if / goto来实现)。

没有线程,您只需实现一个状态引擎。我不得不做很多次(我第一次做,我从未听说过,只是做了一个由“ State”变量控制的大switch语句)。状态机仍然很常见,但可能会令人讨厌。有了线程,大量的样板文件就消失了。

它们还使语言更容易将其运行时执行分解为对多CPU友好的块(我相信Actors也是如此)。 br />
Java在操作系统不提供任何线程支持的系统上提供“绿色”线程。在这种情况下,很容易看到它们显然不过是编程抽象而已。

评论


+1-线程仅提供基本顺序机器上并行性的抽象,就像高级编程语言提供机器代码的抽象一样。

–mouviciel
2011年10月6日上午8:17

#12 楼

操作系统使用时间分片的概念,其中每个线程都有时间运行,然后被抢占。那样的方法可以代替现在的线程,但是在每个应用程序中编写自己的调度程序将是过大的。此外,您还必须使用I / O设备等。并且需要硬件方面的一些支持,以便您可以触发中断以使调度程序运行。基本上,您每次都会编写一个新的操作系统。

通常,在线程等待I / O或睡眠的情况下,线程可以提高性能。它还允许您在执行长任务时使界面具有响应性,并允许停止进程。而且,线程化可以改善真正的多核CPU上的性能。

#13 楼

首先,线程可以同时执行两项或多项操作(如果您拥有多个内核)。尽管您也可以对多个进程执行此操作,但有些任务只是不能很好地分布在多个进程中。

此外,有些任务中有无法避免的空间。例如,很难从磁盘上的文件读取数据,也很难让您的进程同时执行其他操作。如果您的任务必然需要从磁盘读取大量数据,则无论您做什么,您的过程都将花费大量时间等待磁盘。

其次,线程可以使您避免优化大量非关键性能的代码。如果只有一个线程,那么每一段代码都是性能至关重要的。如果阻止,您将沉没-该过程将无法完成的任务。使用线程,阻塞只会影响线程的出现,而其他线程才能进入并执行该进程需要完成的任务。

一个很好的例子是不经常执行的错误处理代码。假设任务遇到了非常少见的错误,并且处理该错误的代码需要分页到内存中。如果磁盘繁忙,并且进程只有一个线程,则在处理该错误的代码可以加载到内存之前,无法进行任何转发。这可能会导致突发响应。

另一个示例是,如果您很少需要执行数据库查找。如果您等待数据库回复,您的代码将遇到巨大的延迟。但是您不希望麻烦使所有这些代码异步,因为这种代码很少见,您需要进行这些查找。有了一个可以完成这项工作的线程,您将两全其美。进行此工作的线程使其对性能的影响不大。