尽管线程可以加快代码执行速度,但实际上是否需要它们?可以使用单个线程来完成每段代码,还是存在只能通过使用多个线程来完成的事情?
尽管线程可以加快代码执行速度,但实际上是否需要它们?可以使用单个线程来完成每段代码,还是存在只能通过使用多个线程来完成的事情?
Answers:
首先,线程无法加快代码执行速度。它们不会使计算机运行得更快。他们所能做的就是利用可能浪费的时间来提高计算机的效率。在某些类型的处理中,此优化可以提高效率并减少运行时间。
简单的答案是。您可以编写要在单个线程上运行的任何代码。证明:单处理器系统只能线性运行指令。通过操作系统处理中断,保存当前线程的状态并启动另一个线程来完成多行执行。
在复杂的答案是...更复杂!多线程程序通常可能比线性程序更高效的原因是由于硬件“问题”。与内存和硬存储IO相比,CPU可以更快地执行计算。因此,例如,“添加”指令的执行速度远快于“获取”指令。缓存和专用程序指令获取(此处不确定确切的术语)可以在某种程度上解决这个问题,但是速度问题仍然存在。
线程是通过在IO指令完成时将CPU用于CPU绑定指令来解决这种不匹配的方法。典型的线程执行计划可能是:获取数据,处理数据,写入数据。出于说明性目的,假设读取和写入需要3个周期,而处理只需要1个周期。您会看到计算机正在读取或写入时,每台计算机都没有执行2个周期的操作吗?显然这很懒,我们需要破解我们的优化工具!
我们可以使用线程重写该过程以使用浪费的时间:
等等。显然,这是一个有些人为的示例,但是您可以看到该技术如何利用原本用于等待IO的时间。
请注意,如上所示的线程处理只能提高IO绑定严重的进程的效率。如果程序主要用于计算内容,那么就不会有很多“漏洞”可以做更多的工作。而且,在线程之间进行切换时,会有多条指令的开销。如果运行的线程过多,则CPU将花费大部分时间进行切换,而在解决问题上却没有太多实际工作。这称为颠簸。
对于单个核心处理器来说,一切都很好,但是大多数现代处理器具有两个或更多核心。线程仍然具有相同的目的-最大限度地利用CPU,但是这次我们可以同时运行两条单独的指令。这可以减少由然而,许多核心的因素运行时间是可用的,因为计算机实际上是多任务,没有上下文切换。
对于多核,线程提供了一种在两个核之间分配工作的方法。以上内容仍然适用于每个核心;一个在一个内核上有两个线程的情况下运行效率最高的程序最有可能在两个内核上有四个线程的情况下以峰值效率运行。(效率是通过最少的NOP指令执行来衡量的。)
在多核(而不是单核)上运行线程的问题通常由硬件解决。CPU将确保在对其进行读/写操作之前锁定适当的存储器位置。(我已经读到它为此在内存中使用了一个特殊的标志位,但是可以通过几种方式来完成。)作为使用高级语言的程序员,您不必担心两个内核上的任何事情将不得不与一个。
TL; DR:线程可以拆分工作,以允许计算机异步处理多个任务。这使计算机可以利用所有可用的处理时间以最大的效率运行,而不是在进程等待资源时锁定。
单个线程不能执行的多个线程有什么作用?
没有。
简单的证明草图:
但是请注意,有一个重要的假设藏在那里:即所使用的语言中的一个线程是图灵完备。
因此,更有趣的问题是:“ 仅将多线程添加到非图灵完备的语言中是否可以使其成为图灵完备的语言?” 我相信答案是“是”。
让我们来看看Total Functional Languages。[对于不熟悉的人:就像功能编程是使用功能编程一样,总功能编程是使用总功能编程。]
全部功能语言显然不是图灵完备的:您不能在TFPL中编写无限循环(实际上,这几乎是“总数” 的定义),但是您可以在图灵机中,至少存在一个程序不能用TFPL编写,但可以用UTM编写,因此TFPL的计算功能不如UTM强大。
但是,一旦将线程添加到TFPL中,就会出现无限循环:只需在新线程中进行循环的每次迭代即可。每个单独的线程总是返回结果,因此它是合计的,但是每个线程也产生一个新的线程,该线程执行下一次迭代,并无限执行。
我认为这种语言将是图灵完备的。
至少,它回答了原始问题:
单个线程不能执行的多个线程有什么作用?
如果您使用的语言无法执行无限循环,那么多线程允许您执行无限循环。
当然,请注意,产生线程是一种副作用,因此我们的扩展语言不仅不再是Total语言,甚至不再是Functional语言。
从理论上讲,多线程程序所做的所有事情都可以用单线程程序完成,只是速度较慢。
在实践中,速度差异可能如此之大,以至于无法将单线程程序用于该任务。例如,如果您有一个每晚运行的批处理数据处理作业,并且在一个线程上花费了24小时以上,那么您别无选择,只能使其成为多线程。(实际上,阈值可能更低:通常,此类更新任务必须在用户再次开始使用系统之前的清晨完成。此外,其他任务可能取决于它们,这些任务也必须在同一晚完成。因此,可用运行时间可能低至几个小时/分钟。)
在多个线程上执行计算工作是一种分布式处理的形式。您将工作分配到多个线程上。分布式处理(使用多台计算机而不是多个线程)的另一个示例是SETI屏幕保护程序:在单个处理器上处理大量测量数据将花费很长时间,并且研究人员希望在退役之前先查看结果;-)但是,他们由于没有这么长时间租用超级计算机的预算,因此他们将工作分配到数百万台家用PC上,使其价格便宜。
尽管线程似乎是顺序计算的一小步,但实际上,它们代表了巨大的一步。他们抛弃了顺序计算的最基本和最吸引人的属性:可理解性,可预测性和确定性。线程作为计算的模型,通常是不确定性的,程序员的工作成为修剪不确定性的方法之一。
-线程问题(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)后来将此称为“ 发痒的龙的尾巴“这就是玩线程的感觉(至少无论如何对我来说)。乍一看似乎很无伤大雅,但是到了被人咬伤时,您就开始抓紧时间变坏了。但是至少线程赢了不要杀了你
首先,单线程应用程序将永远不会利用多核CPU或超线程。但是,即使在单核上,执行多线程的单线程CPU也具有优势。
考虑替代方法,以及这是否会让您感到高兴。假设您有多个任务需要同时运行。例如,您必须保持与两个不同系统的通信。没有多线程怎么办?您可能会创建自己的调度程序,并使其调用需要执行的不同任务。这意味着您需要将任务分成几部分。您可能需要满足一些实时约束,必须确保您的零件不会占用太多时间。否则计时器将在其他任务中过期。这使得拆分任务更加困难。您需要管理的任务越多,需要进行的拆分就越多,满足所有约束的调度程序将变得越复杂。
当您有多个线程时,生活会变得更加轻松。抢先式调度程序可以随时停止线程,保持其状态,然后重新(启动)另一个线程。线程轮到时它将重新启动。优点:编写调度程序的复杂性已经为您完成,您不必拆分任务。而且,调度程序能够管理您自己甚至不知道的进程/线程。而且,当线程不需要执行任何操作(它正在等待某个事件)时,它将不占用任何CPU周期。创建向下的单线程调度程序时,要完成此任务并不容易。(让某物入睡并不难,但是如何唤醒?)
多线程开发的缺点是您需要了解并发问题,锁定策略等。开发无错误的多线程代码可能非常困难。而且调试会更加困难。
线程不仅与速度有关,而且与并发性有关。
如果您没有@Peter建议的批处理应用程序,而是WPF这样的GUI工具包,那么如何仅通过一个线程就可以与用户和业务逻辑进行交互?
另外,假设您正在构建Web服务器。您如何只用一个线程(假设没有其他进程)同时服务多个用户?
在许多情况下,仅一个线程是不够的。这就是为什么最近出现的进步,例如具有50多个内核和数百个线程的Intel MIC处理器。
是的,并行和并发编程很难。但有必要。
多线程可以使GUI界面在长时间的处理操作中仍然能够响应。如果没有多线程,则在长时间运行过程中,用户将无法观看锁定的表单。
无法将还需要保持对其他输入(GUI或其他连接)响应的处理阻塞IO的应用程序设为单线程
在IO库中添加检查方法以查看可以无阻塞地读取多少内容可以帮助实现这一点,但没有多少库对此提供任何完全保证
有很多不错的答案,但我不确定我是否会说出任何短语-也许这提供了另一种查看方式:
线程只是像Objects或Actors或for循环这样的简化程序(是的,可以使用if / goto实现的任何使用循环实现的东西)。
没有线程,您只需实现状态引擎。我不得不做很多次(我第一次做,我从未听说过,只是做了一个由“ State”变量控制的大switch语句)。状态机仍然很普遍,但可能令人讨厌。有了线程,大量的样板就消失了。
它们还碰巧使一种语言更容易将其运行时执行分解为对多CPU友好的块(我相信Actors也是如此)。
Java在操作系统不提供任何线程支持的系统上提供“绿色”线程。在这种情况下,很容易看出它们显然只是编程抽象。
首先,线程可以同时执行两项或多项操作(如果您拥有多个内核)。尽管您也可以对多个流程执行此操作,但是某些任务并不能很好地分布在多个流程中。
另外,有些任务中有一些您无法轻易避免的空间。例如,很难从磁盘上的文件中读取数据,也很难让您的进程同时执行其他操作。如果您的任务必然需要从磁盘读取大量数据,则无论您做什么,您的过程都将花费大量时间等待磁盘。
其次,线程可以让您避免优化非关键性能的大量代码。如果只有一个线程,那么每段代码都是至关重要的。如果阻止,您将沉没-该流程将无法完成的任务。使用线程时,阻塞只会影响该线程,其他线程可以进入并执行该进程需要完成的任务。
一个很好的例子是不经常执行的错误处理代码。假设任务遇到了非常少见的错误,并且处理该错误的代码需要分页到内存中。如果磁盘繁忙,并且该进程只有一个线程,则在处理该错误的代码可以加载到内存之前,无法进行任何转发进度。这可能会导致突发响应。
另一个示例是,如果您很少需要执行数据库查找。如果您等待数据库回复,则您的代码将遇到巨大的延迟。但是您不希望麻烦使所有这些代码异步,因为这种代码很少见,您需要进行这些查找。有了一个可以完成这项工作的线程,您将两全其美。进行此工作的线程使其对性能的影响不大。