1 并发模型

并发系统能够采用多种并发编程模型来实现。并发模型指定了系统中的线程如何经过协做来完成分配给它们的做业。不一样的并发模型采用不一样的方式拆分做业,同时线程间的协做和交互方式也不相同。这篇并发模型教程将会较深刻地介绍目前(2015年,本文撰写时间)比较流行的几种并发模型。
html

并发模型与分布式系统之间的类似性

本文所描述的并发模型相似于分布式系统中使用的不少体系结构。在并发系统中线程之间能够相互通讯。在分布式系统中进程之间也能够相互通讯(进程有可能在不一样的机器中)。线程和进程之间具备不少类似的特性。这也就是为何不少并发模型一般相似于各类分布式系统架构。java

固然,分布式系统在处理网络失效、远程主机或进程宕掉等方面也面临着额外的挑战。可是运行在巨型服务器上的并发系统也可能遇到相似的问题,好比一块CPU失效、一块网卡失效或一个磁盘损坏等状况。虽然出现失效的几率可能很低,可是在理论上仍然有可能发生。web

因为并发模型相似于分布式系统架构,所以它们一般能够互相借鉴思想。例如,为工做者们(线程)分配做业的模型通常与分布式系统中的负载均衡系统比较类似。一样,它们在日志记录、失效转移、幂等性等错误处理技术上也具备类似性。
【注:幂等性,一个幂等操做的特色是其任意屡次执行所产生的影响均与一次执行的影响相同】算法

并行工做者

第一种并发模型就是我所说的并行工做者模型传入的做业会被分配到不一样的工做者上。下图展现了并行工做者模型:数据库

在并行工做者模型中,委派者(Delegator)将传入的做业分配给不一样的工做者每一个工做者完成整个任务。工做者们并行运做在不一样的线程上,甚至可能在不一样的CPU上。编程

若是在某个汽车厂里实现了并行工做者模型,每台车都会由一个工人来生产。工人们将拿到汽车的生产规格,而且从头至尾负责全部工做。数组

在Java应用系统中,并行工做者模型是最多见的并发模型(即便正在转变)。java.util.concurrent包中的许多并发实用工具都是设计用于这个模型的。你也能够在Java企业级(J2EE)应用服务器的设计中看到这个模型的踪影。缓存

并行工做者模型的优势

并行工做者模式的优势是,它很容易理解。你只需添加更多的工做者来提升系统的并行度。服务器

例如,若是你正在作一个网络爬虫,能够试试使用不一样数量的工做者抓取到必定数量的页面,而后看看多少数量的工做者消耗的时间最短(意味着性能最高)。因为网络爬虫是一个IO密集型工做,最终结果颇有多是你电脑中的每一个CPU或核心分配了几个线程。每一个CPU若只分配一个线程可能有点少,由于在等待数据下载的过程当中CPU将会空闲大量时间。网络

并行工做者模型的缺点

并行工做者模型虽然看起来简单,却隐藏着一些缺点。接下来的章节中我会分析一些最明显的弱点。

共享状态可能会很复杂

在实际应用中,并行工做者模型可能比前面所描述的状况要复杂得多。共享的工做者常常须要访问一些共享数据,不管是内存中的或者共享的数据库中的。下图展现了并行工做者模型是如何变得复杂的:

有些共享状态是在像做业队列这样的通讯机制下。但也有一些共享状态是业务数据,数据缓存,数据库链接池等。

一旦共享状态潜入到并行工做者模型中,将会使状况变得复杂起来。线程须要以某种方式存取共享数据,以确保某个线程的修改可以对其余线程可见(数据修改须要同步到主存中,不只仅将数据保存在执行这个线程的CPU的缓存中)。线程须要避免竟态死锁以及不少其余共享状态的并发性问题。

此外,在等待访问共享数据结构时,线程之间的互相等待将会丢失部分并行性。许多并发数据结构是阻塞的,意味着在任何一个时间只有一个或者不多的线程可以访问。这样会致使在这些共享数据结构上出现竞争状态。在执行须要访问共享数据结构部分的代码时,高竞争基本上会致使执行时出现必定程度的串行化。

如今的非阻塞并发算法也许能够下降竞争并提高性能可是非阻塞算法的实现比较困难。

可持久化的数据结构是另外一种选择。在修改的时候,可持久化的数据结构老是保护它的前一个版本不受影响。所以,若是多个线程指向同一个可持久化的数据结构,而且其中一个线程进行了修改,进行修改的线程会得到一个指向新结构的引用。全部其余线程保持对旧结构的引用,旧结构没有被修改而且所以保证一致性。Scala编程包含几个持久化数据结构。
【注:这里的可持久化数据结构不是指持久化存储,而是一种数据结构,好比Java中的String类,以及CopyOnWriteArrayList类,具体可参考

虽然可持久化的数据结构在解决共享数据结构的并发修改时显得很优雅,可是可持久化的数据结构的表现每每不尽人意。

好比说,一个可持久化的链表须要在头部插入一个新的节点,而且返回指向这个新加入的节点的一个引用(这个节点指向了链表的剩余部分)。全部其余现场仍然保留了这个链表以前的第一个节点,对于这些线程来讲链表仍然是为改变的。它们没法看到新加入的元素。

这种可持久化的列表采用链表来实现。不幸的是链表在现代硬件上表现的不太好。链表中得每一个元素都是一个独立的对象,这些对象能够遍及在整个计算机内存中。现代CPU可以更快的进行顺序访问,因此你能够在现代的硬件上用数组实现的列表,以得到更高的性能。数组能够顺序的保存数据。CPU缓存可以一次加载数组的一大块进行缓存,一旦加载完成CPU就能够直接访问缓存中的数据。这对于元素散落在RAM中的链表来讲,不太可能作获得。

无状态的工做者

共享状态可以被系统中得其余线程修改。因此工做者在每次须要的时候必须重读状态以确保每次都能访问到最新的副本,无论共享状态是保存在内存中的仍是在外部数据库中。工做者没法在内部保存这个状态(可是每次须要的时候能够重读)称为无状态的。

每次都重读须要的数据,将会致使速度变慢,特别是状态保存在外部数据库中的时候。

任务顺序是不肯定的

并行工做者模式的另外一个缺点是,做业执行顺序是不肯定的。没法保证哪一个做业最早或者最后被执行。做业A可能在做业B以前就被分配工做者了,可是做业B反而有可能在做业A以前执行。

并行工做者模式的这种非肯定性的特性,使得很难在任何特定的时间点推断系统的状态。这也使得它也更难(若是不是不可能的话)保证一个做业在其余做业以前被执行。

流水线模式

第二种并发模型咱们称之为流水线并发模型。我之因此选用这个名字,只是为了配合“并行工做者”的隐喻。其余开发者可能会根据平台或社区选择其余称呼(好比说反应器系统,或事件驱动系统)。下图表示一个流水线并发模型:

相似于工厂中生产线上的工人们那样组织工做者。每一个工做者只负责做业中的部分工做。当完成了本身的这部分工做时工做者会将做业转发给下一个工做者。每一个工做者在本身的线程中运行,而且不会和其余工做者共享状态。有时也被成为无共享并行模型

一般使用非阻塞的IO来设计使用流水线并发模型的系统。非阻塞IO意味着,一旦某个工做者开始一个IO操做的时候(好比读取文件或从网络链接中读取数据),这个工做者不会一直等待IO操做的结束。IO操做速度很慢,因此等待IO操做结束很浪费CPU时间。此时CPU能够作一些其余事情。当IO操做完成的时候,IO操做的结果(好比读出的数据或者数据写完的状态)被传递给下一个工做者。

有了非阻塞IO,就可使用IO操做肯定工做者之间的边界。工做者会尽量多运行直到遇到并启动一个IO操做。而后交出做业的控制权。当IO操做完成的时候,在流水线上的下一个工做者继续进行操做,直到它也遇到并启动一个IO操做。

在实际应用中,做业有可能不会沿着单一流水线进行。因为大多数系统能够执行多个做业,做业从一个工做者流向另外一个工做者取决于做业须要作的工做。在实际中可能会有多个不一样的虚拟流水线同时运行。这是现实当中做业在流水线系统中可能的移动状况:

做业甚至也有可能被转发到超过一个工做者上并发处理。好比说,做业有可能被同时转发到做业执行器和做业日志器。下图说明了三条流水线是如何经过将做业转发给同一个工做者(中间流水线的最后一个工做者)来完成做业:

流水线有时候比这个状况更加复杂。

反应器,事件驱动系统

采用流水线并发模型的系统有时候也称为反应器系统或事件驱动系统。系统内的工做者对系统内出现的事件作出反应,这些事件也有可能来自于外部世界或者发自其余工做者。事件能够是传入的HTTP请求,也能够是某个文件成功加载到内存中等。在写这篇文章的时候,已经有不少有趣的反应器/事件驱动平台可使用了,而且不久的未来会有更多。比较流行的彷佛是这几个:

  • Vert.x
  • AKKa
  • Node.JS(JavaScript)

我我的以为Vert.x是至关有趣的(特别是对于我这样使用Java/JVM的人来讲)

Actors 和 Channels

Actors 和 channels 是两种比较相似的流水线(或反应器/事件驱动)模型。

在Actor模型中每一个工做者被称为actor。Actor之间能够直接异步地发送和处理消息。Actor能够被用来实现一个或多个像前文描述的那样的做业处理流水线。下图给出了Actor模型:

而在Channel模型中,工做者之间不直接进行通讯。相反,它们在不一样的通道中发布本身的消息(事件)。其余工做者们能够在这些通道上监听消息,发送者无需知道谁在监听。下图给出了Channel模型:

在写这篇文章的时候,channel模型对于我来讲彷佛更加灵活。一个工做者无需知道谁在后面的流水线上处理做业。只需知道做业(或消息等)须要转发给哪一个通道。通道上的监听者能够随意订阅或者取消订阅,并不会影响向这个通道发送消息的工做者。这使得工做者之间具备松散的耦合。

流水线模型的优势

相比并行工做者模型,流水线并发模型具备几个优势,在接下来的章节中我会介绍几个最大的优势。

无需共享的状态

工做者之间无需共享状态,意味着实现的时候无需考虑全部因并发访问共享对象而产生的并发性问题。这使得在实现工做者的时候变得很是容易。在实现工做者的时候就好像是单个线程在处理工做-基本上是一个单线程的实现

有状态的工做者

当工做者知道了没有其余线程能够修改它们的数据,工做者能够变成有状态的。对于有状态,我是指,它们能够在内存中保存它们须要操做的数据,只需在最后将更改写回到外部存储系统。所以,有状态的工做者一般比无状态的工做者具备更高的性能。

较好的硬件整合(Hardware Conformity)

单线程代码在整合底层硬件的时候每每具备更好的优点。首先,当能肯定代码只在单线程模式下执行的时候,一般可以建立更优化的数据结构和算法

其次,像前文描述的那样,单线程有状态的工做者可以在内存中缓存数据。在内存中缓存数据的同时,也意味着数据颇有可能也缓存在执行这个线程的CPU的缓存中。这使得访问缓存的数据变得更快。

我说的硬件整合是指,以某种方式编写的代码,使得可以天然地受益于底层硬件的工做原理。有些开发者称之为mechanical sympathy。我更倾向于硬件整合这个术语,由于计算机只有不多的机械部件,而且可以隐喻“更好的匹配(match better)”,相比“同情(sympathy)”这个词在上下文中的意思,我以为“conform”这个词表达的很是好。固然了,这里有点吹毛求疵了,用本身喜欢的术语就行。

合理的做业顺序

基于流水线并发模型实现的并发系统,在某种程度上是有可能保证做业的顺序的做业的有序性使得它更容易地推出系统在某个特定时间点的状态。更进一步,你能够将全部到达的做业写入到日志中去。一旦这个系统的某一部分挂掉了,该日志就能够用来重头开始重建系统当时的状态。按照特定的顺序将做业写入日志,并按这个顺序做为有保障的做业顺序。下图展现了一种可能的设计:

实现一个有保障的做业顺序是不容易的,但每每是可行的。若是能够,它将大大简化一些任务,例如备份、数据恢复、数据复制等,这些均可以经过日志文件来完成。

流水线模型的缺点

流水线并发模型最大的缺点是做业的执行每每分布到多个工做者上,并所以分布到项目中的多个类上。这样致使在追踪某个做业到底被什么代码执行时变得困难。

一样,这也加大了代码编写的难度。有时会将工做者的代码写成回调处理的形式。若在代码中嵌入过多的回调处理,每每会出现所谓的回调地狱(callback hell)现象。所谓回调地狱,就是意味着在追踪代码在回调过程当中到底作了什么,以及确保每一个回调只访问它须要的数据的时候,变得很是困难

使用并行工做者模型能够简化这个问题。你能够打开工做者的代码,从头至尾优美的阅读被执行的代码。固然并行工做者模式的代码也可能一样分布在不一样的类中,但每每也可以很容易的从代码中分析执行的顺序。

函数式并行(Functional Parallelism)

第三种并发模型是函数式并行模型,这是也最近(2015)讨论的比较多的一种模型。函数式并行的基本思想是采用函数调用实现程序。函数能够看做是”代理人(agents)“或者”actor“,函数之间能够像流水线模型(AKA 反应器或者事件驱动系统)那样互相发送消息某个函数调用另外一个函数,这个过程相似于消息发送。

函数都是经过拷贝来传递参数的,因此除了接收函数外没有实体能够操做数据这对于避免共享数据的竞态来讲是颇有必要的。一样也使得函数的执行相似于原子操做。每一个函数调用的执行独立于任何其余函数的调用。

一旦每一个函数调用均可以独立的执行,它们就能够分散在不一样的CPU上执行了。这也就意味着可以在多处理器上并行的执行使用函数式实现的算法。

Java7中的java.util.concurrent包里包含的ForkAndJoinPool可以帮助咱们实现相似于函数式并行的一些东西。而Java8中并行streams可以用来帮助咱们并行的迭代大型集合。记住有些开发者对ForkAndJoinPool进行了批判(你能够在个人ForkAndJoinPool教程里面看到批评的连接)。

函数式并行里面最难的是肯定须要并行的那个函数调用。跨CPU协调函数调用须要必定的开销。某个函数完成的工做单元须要达到某个大小以弥补这个开销。若是函数调用做用很是小,将它并行化可能比单线程、单CPU执行还慢。

我我的认为(可能不太正确),你可使用反应器或者事件驱动模型实现一个算法,像函数式并行那样的方法实现工做的分解。使用事件驱动模型能够更精确的控制如何实现并行化(个人观点)。

此外,将任务拆分给多个CPU时协调形成的开销,仅仅在该任务是程序当前执行的惟一任务时才有意义。可是,若是当前系统正在执行多个其余的任务时(好比web服务器,数据库服务器或者不少其余相似的系统),将单个任务进行并行化是没有意义的。无论怎样计算机中的其余CPU们都在忙于处理其余任务,没有理由用一个慢的、函数式并行的任务去扰乱它们。使用流水线(反应器)并发模型可能会更好一点,由于它开销更小(在单线程模式下顺序执行)同时能更好的与底层硬件整合。

使用那种并发模型最好?

因此,用哪一种并发模型更好呢?

一般状况下,这个答案取决于你的系统打算作什么。若是你的做业自己就是并行的、独立的而且没有必要共享状态,你可能会使用并行工做者模型去实现你的系统。虽然许多做业都不是天然并行和独立的。对于这种类型的系统,我相信使用流水线并发模型可以更好的发挥它的优点,并且比并行工做者模型更有优点。

你甚至不用亲自编写全部流水线模型的基础结构。像Vert.x这种现代化的平台已经为你实现了不少。我也会去为探索如何设计个人下一个项目,使它运行在像Vert.x这样的优秀平台上。我感受Java EE已经没有任何优点了。

 

转自:http://ifeve.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E6%A8%A1%E5%9E%8B/

相关文章
相关标签/搜索