第二种并发模型我们称之为流水线并发模型。我之所以选用这个名字,只是为了配合“并行工作者”的隐喻。其他开发者可能会根据平台或社区选择其他称呼(比如说反应器系统,或事件驱动系统)。
下图表示一个流水线并发模型:
![d80b787c086fb9cbd365a4942f530b5d.png](https://i-blog.csdnimg.cn/blog_migrate/a7299f6b1b32e6b98751506dc4e8b365.jpeg)
类似于工厂中生产线上的工人们那样组织工作者。每个工作者只负责作业中的部分工作。当完成了自己的这部分工作时工作者会将作业转发给下一个工作者。每个工作者在自己的线程中运行,并且不会和其他工作者共享状态。有时也被称为无共享并行模型。
通常使用非阻塞的 IO 来设计使用流水线并发模型的系统。非阻塞 IO 意味着,一旦某个工作者开始一个 IO 操作的时候(比如读取文件或从网络连接中读取数据),这个工作者不会一直等待 IO 操作的结束。IO 操作速度很慢,所以等待 IO 操作结束很浪费 CPU 时间。此时 CPU 可以做一些其他事情。当 IO 操作完成的时候,IO 操作的结果(比如读出的数据或者数据写完的状态)被传递给下一个工作者。
有了非阻塞 IO,就可以使用 IO 操作确定工作者之间的边界。工作者会尽可能多运行直到遇到并启动一个 IO 操作。然后交出作业的控制权。当 IO 操作完成的时候,在流水线上的下一个工作者继续进行操作,直到它也遇到并启动一个 IO 操作。
![256c9237425f683431b78deb1787c995.png](https://i-blog.csdnimg.cn/blog_migrate/f3e5662066633ecca80b1f29f34b9138.jpeg)
在实际应用中,作业有可能不会沿着单一流水线进行。由于大多数系统可以执行多个作业,作业从一个工作者流向另一个工作者取决于作业需要做的工作。在实际中可能会有多个不同的虚拟流水线同时运行。这是现实当中作业在流水线系统中可能的移动情况:
![6b2c650560faf63e1d96c4e8bbfd557c.png](https://i-blog.csdnimg.cn/blog_migrate/577c11b43c45e5d219e6a829ecaf787f.jpeg)
作业甚至也有可能被转发到超过一个工作者上并发处理。比如说,作业有可能被同时转发到作业执行器和作业日志器。下图说明了三条流水线是如何通过将作业转发给同一个工作者(中间流水线的最后一个工作者)来完成作业:
![4f21f259c21e6a6d930227849bbc2fba.png](https://i-blog.csdnimg.cn/blog_migrate/d7396ec44045596962d9748fed1947ac.jpeg)
流水线有时候比这个情况更加复杂。
反应器,事件驱动系统
采用流水线并发模型的系统有时候也称为反应器系统或事件驱动系统。系统内的工作者对系统内出现的事件做出反应,这些事件也有可能来自于外部世界或者发自其他工作者。事件可以是传入的 HTTP 请求,也可以是某个文件成功加载到内存中等。
现在,已经有很多有趣的反应器/事件驱动平台可以使用了,并且不久的将来会有更多。
比较流行的似乎是这几个:
- Vert.x
- AKKa
- Node.JS(JavaScript)
我个人觉得 Vert.x 是相当有趣的(特别是对于我这样使用 Java/JVM 的人来说)
Actors 和 Channels
Actors 和 channels 是两种比较类似的流水线(或反应器/事件驱动)模型。
在 Actor 模型中每个工作者被称为 actor。Actor 之间可以直接异步地发送和处理消息。Actor 可以被用来实现一个或多个像前文描述的那样的作业处理流水线。
下图给出了 Actor 模型:
![a727b4d7cd35cfc37ee3016b55190b10.png](https://i-blog.csdnimg.cn/blog_migrate/61da168ae48ccb5ba6f95e82de8702db.jpeg)
而在 Channel 模型中,工作者之间不直接进行通信。相反,它们在不同的通道中发布自己的消息(事件)。其他工作者们可以在这些通道上监听消息,发送者无需知道谁在监听。
下图给出了 Channel 模型:
![9766968a75c7cd8960dbcc34c4d27c8a.png](https://i-blog.csdnimg.cn/blog_migrate/48837ddc845920c92f4842986902102c.jpeg)
现在,channel 模型对于我来说似乎更加灵活。一个工作者无需知道谁在后面的流水线上处理作 业。只需知道作业(或消息等)需要转发给哪个通道。通道上的监听者可以随意订阅或者取消订阅,并不会影响向这个通道发送消息的工作者。这使得工作者之间具有松散的耦合。
流水线模型的优点
一.无需共享的状态
工作者之间无需共享状态,意味着实现的时候无需考虑所有因并发访问共享对象而产生的并发性问题。这使得在实现工作者的时候变得非常容易。在实现工作者的时候就好像是单个线程在处理工作-基本上是一个单线程的实现。
二.有状态的工作者
当工作者知道了没有其他线程可以修改它们的数据,工作者可以变成有状态的。对于有状态,我是指,它们可以在内存中保存它们需要操作的数据,只需在最后将更改写回到外部存储系统。因此,有状态的工作者通常比无状态的工作者具有更高的性能。
三.较好的硬件整合(Hardware Conformity)
单线程代码在整合底层硬件的时候往往具有更好的优势。首先,当能确定代码只在单线程模式下执行的时候,通常能够创建更优化的数据结构和算法。
其次,像前文描述的那样,单线程有状态的工作者能够在内存中缓存数据。在内存中缓存数据的同时,也意味着数据很有可能也缓存在执行这个线程的 CPU 的缓存中。这使得访问缓存的数据变得更快。
我说的硬件整合是指,以某种方式编写的代码,使得能够自然地受益于底层硬件的工作原理。有些开发者称之为 mechanical sympathy。我更倾向于硬件整合这个术语,因为计算机只有很少的机械部件,并且能够隐喻“更好的匹配(match better)”,相比“同情(sympathy)”这个词在上下文中的意思,我觉得“conform”这个词表达的非常好。当然了,这里有点吹毛求疵了,用自己喜欢的术语就行。
四.合理的作业顺序
基于流水线并发模型实现的并发系统,在某种程度上是有可能保证作业的顺序的。作业的有序性使得它更容易地推出系统在某个特定时间点的状态。更进一步,你可以将所有到达的作业写入到日志中去。一旦这个系统的某一部分挂掉了,该日志就可以用来重头开始重建系统当时的状态。按照特定的顺序将作业写入日志,并按这个顺序作为有保障的作业顺序。下图展示了一种可能的设计:
![f987f447c2f89972310168c0c10ca865.png](https://i-blog.csdnimg.cn/blog_migrate/b354493ce9f3e1211d4c6da80d52bfb1.jpeg)
实现一个有保障的作业顺序是不容易的,但往往是可行的。如果可以,它将大大简化一些任务,例如备份、数据 恢复、数据复制等,这些都可以通过日志文件来完成。
流水线模型的缺点
流水线并发模型最大的缺点是作业的执行往往分布到多个工作者上,并因此分布到项目中的多个类上。这样导致在追踪某个作业到底被什么代码执行时变得困难。
同样,这也加大了代码编写的难度。有时会将工作者的代码写成回调处理的形式。若在代码中嵌入过多的回调处理,往往会出现所谓的回调地狱(callback hell)现象。所谓回调地狱,就是意味着在追踪代码在回调过程中到底做了什么,以及确保每个回调只访问它需要的数据的时候,变得非常困难.
使用并行工作者模型可以简化这个问题。你可以打开工作者的代码,从头到尾优美的阅读被执行的代码。当然并行工作者模式的代码也可能同样分布在不同的类中,但往往也能够很容易的从代码中分析执行的顺序。