我有将近3年使用MVC框架(如struts)用Java编写Web应用程序的经验。尽管我已经为大型零售连锁店编写了代码,但到目前为止,我从未编写过多线程代码。
在面试过程中,我会遇到一些有关多线程的问题,我通常会回答它们(大多数是简单的问题)。这让我想知道在当前的行业场景中多线程有多重要?
我有将近3年使用MVC框架(如struts)用Java编写Web应用程序的经验。尽管我已经为大型零售连锁店编写了代码,但到目前为止,我从未编写过多线程代码。
在面试过程中,我会遇到一些有关多线程的问题,我通常会回答它们(大多数是简单的问题)。这让我想知道在当前的行业场景中多线程有多重要?
Answers:
这是非常重要的。
不过,更重要的是要了解多线程仅仅是解决异步问题的一种方法。现在许多人正在编写软件的技术环境与历史软件开发环境(执行批处理计算的整体应用程序)在两个关键方面有所不同:
现在,多核计算机很常见。我们不再期望时钟速度或晶体管密度增加几个数量级。计算的价格将继续下降,但由于大量的并行性而将下降。我们将必须找到一种利用这种力量的方法。
现在,计算机已高度联网,现代应用程序依赖于能够从各种来源获取丰富信息的能力。
从计算的角度来看,这两个因素本质上可以归结为相同的核心思想:信息将越来越以异步方式获得。所需的信息是在计算机上的另一个芯片上还是在世界另一端的芯片上计算的,都没有关系。无论哪种方式,您的处理器都坐在那里每秒燃烧数十亿个周期,以等待可能正在做有用工作的信息。
因此,现在重要的事情,以及将来甚至更重要的事情,不是多线程本身,而是异步处理。多线程只是做到这一点的一种方法-一种复杂的,容易出错的方法,随着弱内存模型芯片的广泛使用,它只会变得更加复杂,更容易出错。
工具供应商所面临的挑战是,要想出一种比多线程更好的方法来让我们的客户处理将来将要使用的异步基础结构。
随着现代处理器具有越来越多的内核,它变得越来越重要。十年前,大多数现有计算机只有一个处理器,因此多线程仅在高端服务器应用程序上很重要。如今,即使是基本的笔记本电脑也都具有多核处理器。在几年甚至移动设备中……因此,需要更多的代码来利用并发的潜在性能优势并在多线程环境中正确运行。
总的来说,多线程已经非常重要,并且只会在未来几年变得越来越重要(如PéterTörök所指出的)-这就是处理器在可预见的未来将如何扩展(更多的内核而不是更高的MHz) 。
但是,就您而言,您似乎主要是在使用Web应用程序。Web应用程序本质上是多线程的,这是由于Web服务器处理每个用户(即并行)的请求的方式。虽然了解并发性和线程安全性(特别是在处理缓存和其他共享数据时)对您来说可能很重要,但我怀疑您会遇到太多情况,在内部对Web应用程序代码进行多线程化(例如,多个工作程序)会有所帮助每个请求的线程数)。从这个意义上讲,我认为对于Web开发人员而言,真正不需要多线程专家。在面试中经常会问到这个问题,因为这是一个棘手的问题,而且还因为许多面试官在到达那里10分钟之前就提出了几个问题。
多线程是一个红鲱鱼。多线程是对实际问题Concurrency的实现细节。并非所有线程程序都是由于锁而并发的,而并非因为锁。
线程只是用于实现concurrent
程序的一种模型和实现模式。
例如,您可以编写高度可扩展且具有容错功能的软件,而无需每次都使用诸如Erlang之类的语言进行任何多线程处理。
简短的回答:非常。
更长的答案:电子(基于晶体管的)计算机正在迅速接近技术的物理极限。在管理热量产生和微观电路的量子效应(电路路径已经在现代芯片上如此紧密地放置在一起,以至于被称为“量子隧穿”的效应可以使电子产生)的同时,越来越难以从每个内核中挤出更多的时钟。无需将传统电弧的适当条件“从轨道上跳到另一电路”;因此,实际上,所有芯片制造商都在通过在每个CPU中放置更多的“执行单元”来专注于使每个时钟能够执行更多的工作。然后,计算机可以每个时钟执行2或4甚至8个操作,而不是计算机每个时钟仅执行一项操作,而Intel具有“ HyperThreading”功能,基本上将一个CPU内核拆分为两个逻辑处理器(有一些限制)。几乎所有制造商都将至少两个单独的CPU内核放入一个CPU芯片中,而当前台式机CPU的黄金标准是每个芯片四个内核。当使用两个CPU芯片时,可能有八个。有为“四核”处理器设计的服务器主板(16个EU加可选的HT),下一代CPU可能每个芯片有六个或八个。
所有这些的结果是,要充分利用计算机获得计算能力的方式,您必须能够允许计算机“分而治之”您的程序。托管语言至少具有一个GC线程,该线程与程序分开处理内存管理。有些还具有处理COM / OLE互操作的“转换”线程(与保护托管“沙盒”和性能一样多)。但是,除此之外,您还必须真正开始思考程序如何同时执行多项操作,并以允许异步处理程序段的功能来构造程序。Windows和Windows用户实际上希望您的程序在后台线程中执行长时间,复杂的任务,这样可以使程序的UI(在程序的主线程中运行)对Windows消息循环“响应”。显然,具有可并行化解决方案(例如排序)的问题是自然的候选者,但是受益于并行化的问题种类有限。
只是关于多线程的警告:更多线程并不意味着更高的效率。如果管理不当,它们可能会降低系统速度。Scala的参与者改进了Java的线程并最大限度地提高了系统使用率(因为您是Java开发人员,所以提到了它)。
编辑: 这是多线程的缺点要记住的一些事情:
同样,此链接可能对相同有一定帮助。
这让我想知道在当前行业场景中多线程处理有多重要?
在性能至关重要的领域中,性能不是来自第三方代码的繁琐工作,而是我们自己的性能,因此我倾向于从CPU的角度考虑重要性的顺序(GPU是我赢得的通配符)。不参加):
请注意,此列表不仅仅基于重要性,还包括许多其他动态因素,例如它们对维护的影响,它们的直接程度(如果不是,则需要提前考虑),它们与列表中其他成员的交互作用等。
记忆效率
大多数人可能会对我选择的内存效率超过算法感到惊讶。这是因为内存效率与该列表上的所有其他4个项目相互作用,并且是因为通常在“设计”类别中而不是在“实现”类别中对此进行考虑。诚然,这里存在一些鸡或蛋的问题,因为了解内存效率通常需要考虑列表中的所有4个项目,而所有其他4个项目也都需要考虑内存效率。但这是一切的核心。
例如,如果我们需要一种数据结构,该结构提供线性时间顺序访问和向后固定插入时间,而对于小元素则没有其他选择,那么此处要达到的天真的选择将是链表。这不考虑内存效率。当我们考虑混合中的内存效率时,在这种情况下我们最终会选择更多连续的结构,例如基于可增长的基于数组的结构或更多连续的节点(例如,一个节点中存储128个元素)链接在一起,或者至少由池分配器支持的链表。尽管具有相同的算法复杂度,但它们仍具有显着优势。同样,尽管算法复杂度较低,但由于存储效率的原因,我们经常选择数组的快速排序而不是合并排序。
同样,如果我们的内存访问模式本质上如此细粒度和分散,以至于我们最终最大化了错误共享的数量,同时又将代码锁定在最细粒度的级别,那么我们就无法拥有高效的多线程。因此,内存效率乘以效率多线程。这是充分利用线程的前提。
列表上方的每个项目都与数据进行复杂的交互,因此,专注于数据表示方式最终取决于内存效率。上面这些中的每一个都可能因表示或访问数据的不适当方式而成为瓶颈。
内存效率如此重要的另一个原因是,它可以应用于整个代码库。通常,当人们认为低效率是从零散的工作中累积的,这表明他们需要抓住探查器。然而,即使在进行性能分析后,低延迟字段或处理非常有限的硬件的字段实际上仍会发现会话,这些会话表明在代码库中没有明确的热点(时间分散在整个地方),而代码库的分配,复制和使用方式公然低效访问内存。通常,这是整个代码库中唯一一次可能出现性能问题,这可能会导致在整个代码库中应用一套全新的标准,而内存效率通常是其核心。
算法化
这几乎是给定的,因为排序算法中的选择可以使大量的输入(需要数月的排序时间和数秒的排序时间)有所不同。如果选择在真正低于标准的二次或三次算法与线性算法之间,或者在线性与对数算法或常数之间进行选择,那么它将产生最大的影响,至少直到我们拥有一百万台核心机器(在这种情况下,内存为效率将变得更加重要)。
但是,它不在我的个人列表的顶部,因为该领域的任何人都知道会使用加速结构进行视锥剔除,例如,我们对算法的知识非常了解,并且知道使用诸如trie的变体之类的东西,例如用于基于前缀的搜索的基数树是婴儿的东西。缺乏我们正在研究的领域的这种基础知识,那么算法效率肯定会上升到最高,但是算法效率通常是微不足道的。
在某些领域,还必须发明新的算法(例如:在网格处理中,我不得不发明数百种,因为它们以前不存在,或者其他产品中类似功能的实现是专有秘密,未在论文中公开) )。但是,一旦我们解决了问题,找到了获得正确结果的方法,而一旦效率成为目标,那么真正获得收益的唯一方法就是考虑我们如何与数据(内存)进行交互。如果不了解内存效率,那么新算法可能会不必要地变得复杂,而徒劳地努力使其变得更快,而它所需要的只是稍微考虑一下内存效率以产生一个更简单,更优雅的算法。
最后,算法倾向于更多地属于“实现”类别而不是内存效率。即使使用最初使用的次优算法,通常也更容易改进事后分析。例如,劣等图像处理算法通常只是在代码库的一个本地位置实现。以后可以将其换成更好的。但是,如果所有图像处理算法都绑定到Pixel
具有次优内存表示的接口,但是纠正该接口的唯一方法是更改表示多个像素(而不是单个像素)的方式,那么我们经常SOL,并且必须完全将代码库改写为Image
接口。替换排序算法的方法也一样-通常是实现细节,而对要排序的数据的基本表示形式或通过消息传递的方式的完整更改可能需要重新设计接口。
多线程
在性能方面,多线程是一项艰巨的任务,因为它是一种针对硬件特性的微级优化,但是我们的硬件确实朝着这个方向扩展。我已经有拥有32个核心的同龄人(我只有4个核心)。
但是,如果目的是为了加速软件,则多线程处理是专业人员可能知道的最危险的微优化之一。竞态条件几乎是最致命的错误,因为它本质上是不确定性的(可能在调试环境之外的最不方便的时间,每隔几个月才在开发人员的机器上显示一次,如果有的话)。因此,在所有这些方面,可以说对代码的可维护性和潜在正确性造成的负面影响最大,特别是因为与多线程相关的错误即使在最仔细的测试下也很容易就飞走。
然而,它变得如此重要。考虑到我们现在拥有的内核数量,它可能仍然不总是比内存效率(有时可以使事情快一百倍)之类的东西更重要,但我们看到的内核越来越多。当然,即使使用100核计算机,我仍然将内存效率放在首位,因为没有它通常是不可能实现线程效率的。程序可以在这样的机器上使用一百个线程,但仍然很慢,缺乏有效的内存表示和访问模式(这将与锁定模式相关联)。
SIMD
SIMD也有点尴尬,因为寄存器实际上正在变宽,并计划进一步变宽。最初,我们看到64位MMX寄存器,然后是能够并行执行4个SPFP操作的128位XMM寄存器。现在我们看到256位YMM寄存器能够并行处理8个。并且已经有针对512位寄存器的计划,这将允许16个并行。
这些将交互并与多线程的效率相乘。然而,SIMD会像多线程一样降低可维护性。尽管与它们相关的错误不一定像死锁或竞争条件那样难于再现和修复,但可移植性却很尴尬,确保代码可以在每个人的机器上运行(并根据其硬件功能使用适当的指令)是尴尬。
另一件事是,尽管当今的编译器通常不会击败专家编写的SIMD代码,但它们确实可以轻松击败幼稚的尝试。它们可能会改进到我们不再需要手动执行的程度,或者至少无需手动编写内在代码或直接汇编代码即可(也许只是一些人工指导)。
同样,如果没有有效进行矢量化处理的内存布局,SIMD毫无用处。我们最终将只将一个标量字段加载到一个宽寄存器中,然后对其进行一次操作。所有这些项目的核心是对内存布局的依赖,以使其真正有效。
其他优化
这些通常是我现在建议我们开始使用的“微型”,如果这个词不仅暗示着超越算法重点,而且还朝着对性能影响很小的变化。
通常,尝试针对分支预测进行优化需要更改算法或内存效率,例如,如果仅通过提示并重新排列静态预测代码来尝试这样做,则只会提高此类代码的首次执行效率,如果常常不能完全忽略不计。
返回多线程性能
那么,无论如何,从性能上下文来看多线程有多重要?在我的4核计算机上,理想情况下,它可使处理速度提高约5倍(超线程可以达到的速度)。对于拥有32个核心的我的同事而言,这将更为重要。在未来的几年中它将变得越来越重要。
所以这很重要。但是,如果没有足够的内存效率以允许少量使用锁,减少错误共享等,仅抛出一堆线程是没有用的。
性能之外的多线程
从直接吞吐量的意义上讲,多线程并不总是与纯粹的性能有关。有时,它甚至可以以可能的吞吐量成本来平衡负载,以提高对用户的响应速度,或者允许用户执行更多的多任务处理而不必等待事情完成(例如:在下载文件的同时继续浏览)。
在这种情况下,我建议多线程向顶端上升(甚至可能超过内存效率),因为那是关于用户端设计,而不是充分利用硬件。在这种情况下,它将经常主导界面设计以及我们构建整个代码库的方式。
当我们不只是并行化一个访问大规模数据结构的紧密循环时,多线程进入了真正的“设计”类别,而设计总是比实现更为重要。
因此,在这种情况下,我想说考虑多线程是绝对关键的,甚至比内存表示和访问更重要。
从历史上看,人们不得不手工完成多线程编程。他们必须直接使用所有核心组件(线程,信号量,互斥体,锁等)。
所有这些努力使得应用程序能够通过向单个系统添加额外的cpus进行扩展。这种垂直可扩展性受到“我能买到的最大的服务器”的限制。
如今,我看到了朝着使用更多框架和不同设计模型进行软件设计的转变。MapReduce就是这样一种模型,它专注于批处理。
目标是水平扩展。添加更多标准服务器,而不是购买更大的服务器。
话虽如此,但真正了解多线程编程仍然非常重要。我一直处于这样的情况,有人创建了竞争条件,甚至不知道竞争条件是什么,直到我们在测试过程中注意到奇怪的错误。
我的机器有8个核心。在任务管理器中,我有60个进程正在运行。有些像VS,最多使用98个线程。Outlook使用26。我希望我的大部分内存使用情况是分配给每个空闲线程的堆栈。
我个人正在等待300核计算机问世,这样我就不必等待Outlook做出响应。当然,届时Outlook将使用301个线程。
仅当您要构建在特定时间计算机上唯一重要的系统(例如计算引擎)时,多线程才有意义。桌面应用程序可能会因为不耗尽所有可用内核而对用户有所帮助。使用请求/响应模型的Web应用程序本质上是多线程的。
对于框架和语言设计人员以及后端系统程序员而言,这很重要-对应用程序构建者而言并不重要。但是,了解一些基本概念(例如锁定和编写异步代码)可能是值得的。