单个线程不能执行的多个线程有什么作用?[关闭]


100

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


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

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

14
单线程程序无法像多线程程序那样死锁资源。
zzzzBov 2011年

3
答案:在两个内核上运行
Daniel Little'8

6
答:造成死锁。
工作

Answers:


111

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

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

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

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

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

  1. #1获取
  2. 无操作
  3. #2获取
  4. #1完成,处理它
  5. 写#1
  6. #1获取
  7. #2完成,处理它
  8. 写#2
  9. 提取#2

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

请注意,如上所示的线程处理只能提高IO绑定严重的进程的效率。如果程序主要用于计算内容,那么就不会有很多“漏洞”可以做更多的工作。而且,在线程之间进行切换时,会有多条指令的开销。如果运行的线程过多,则CPU将花费大部分时间进行切换,而在解决问题上却没有太多实际工作。这称为颠簸

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

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

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

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


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

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

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

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

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

37

单个线程不能执行的多个线程有什么作用?

没有。

简单的证明草图:

  • [教堂图灵猜想]⇒可以通过万能图灵机计算出所有可以计算的东西。
  • 通用图灵机是单线程的。
  • 因此,所有可以计算的内容都可以由一个线程计算。

但是请注意,有一个重要的假设藏在那里:即所使用的语言的一个线程是图灵完备。

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

让我们来看看Total Functional Languages。[对于不熟悉的人:就像功能编程是使用功能编程一样,总功能编程是使用总功能编程。]

全部功能语言显然不是图灵完备的:您不能在TFPL中编写无限循环(实际上,这几乎是“总数” 的定义),但是您可以在图灵机中,至少存在一个程序不能用TFPL编写,但可以用UTM编写,因此TFPL的计算功能不如UTM强大。

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

认为这种语言将是图灵完备的。

至少,它回答了原始问题:

单个线程不能执行的多个线程有什么作用?

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

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


13
由于您提出了“证明”,因此我无法阻止自己“实际上,...”对您的攻击。我认为您正在滥用可计算性的概念。证明是一件很可爱的事,我明白您的意思,但实际上并没有达到正确的抽象水平。不能够做的事情是不一样的不能够计算,例如,你不能使用你的证据说,因为图灵机可以计算出 “每个可计算函数”,它可以做到在3秒内它。严格来说,没有中断的单线程计算机无法访问I / O,同时使处理器忙于进行计算。
xmm0 2011年

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

1
@ColeJohnson:死锁是实现细节。可见的输出是“不停止”,这很容易通过单线程完成。
Heinzi 2013年

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

1
如果宇宙是单线程编辑的,那么论是什么?你不能与这样的科学争论。
corsiKa 2014年

22

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

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

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


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

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

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

11

尽管线程似乎是顺序计算的一小步,但实际上,它们代表了巨大的一步。他们抛弃了顺序计算的最基本和最吸引人的属性:可理解性,可预测性和确定性。线程作为计算的模型,通常是不确定性的,程序员的工作成为修剪不确定性的方法之一。

-线程问题(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回答显得苍白无力。
2011年

1
下午喝咖啡休息时间
Mike Owens

10

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

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

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

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


9

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

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

如果没有多个CPU /内核,线程仍然可以简化概念上并行运行的代码,例如服务器上的客户端处理,但是没有线程您也可以做同样的事情。


6

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

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

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

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

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


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

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

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

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

6

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


这并不完全正确。从理论上讲,您可以将您的长时间操作分成许多较小的操作,并在两者之间更新事件处理。
塞巴斯蒂安·内格拉苏斯

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

5

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

线程可能会从通常的程序员希望调试的东西中清除一个晦涩的错误,并将其移入领域,在这个领域中,当一个机敏的程序员恰巧只看问题时,会被告知需要用同样的bug来抓住同样的错误的运气。正确的时刻。


4

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

在IO库中添加检查方法以查看可以无阻塞地读取多少内容可以帮助实现这一点,但没有多少库对此提供任何完全保证


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

@keith那 -blocking IO我明确表示阻塞
冲高怪胎

但是,如果您想保持响应能力,为什么还要阻止IO?有没有通常没有非阻塞替代方案的阻塞IO?我想不到。
KeithB 2011年

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

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

4

有很多不错的答案,但我不确定我是否会说出任何短语-也许这提供了另一种查看方式:

线程只是像Objects或Actors或for循环这样的简化程序(是的,可以使用if / goto实现的任何使用循环实现的东西)。

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

它们还碰巧使一种语言更容易将其运行时执行分解为对多CPU友好的块(我相信Actors也是如此)。

Java在操作系统不提供任何线程支持的系统上提供“绿色”线程。在这种情况下,很容易看出它们显然只是编程抽象。


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

0

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

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


0

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

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

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

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

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

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.