引论

并发编程和函数式编程

我们提到响应式编程,常常就会提到并发编程,因为并发环境下的部分问题可以通过响应式的编程模型解决。但是虽然两者的边界有重叠之处,然则内涵并不相同。毕竟解决系统并发互斥问题,通常意义上的解决方案是加锁,而非所谓的响应式编程。如果断然将两者混为一谈可能会造成理解上的偏差。
image.png
我们提到响应式编程,常常还会提到函数式编程,仿佛这是一组固定搭配。因为某种意义上来说,两种编程范式很多概念都有相似之处,例如高阶函数 vs 高阶 Observable,惰性计算 vs 惰性事件,同时 Functional Reactive Programming(响应式函数式编程)概念也是广为人知,两者搭配使用彷佛无往不利。函数式编程常常占有更大的话题热度,起源于计算主义思潮,发展于 Church 的 Lambda 演算,拥有简约并且完备的符号系统,思想深刻然则貌似常见,平日多写几个高阶函数,也会感到沾沾自喜,这就是函数式编程?
image.png
相对而言,响应式编程则会让人感到困惑,什么是响应式编程,为何要响应式编程?作为新手面对这种无中生有总会望而生却。响应式编程没有煊赫的历史渊源,但是有着繁多的使用概念,如果想要彻底了解并且掌握使用场景,确实需要一些学习成本。

编程范式

响应式编程式一种编程范式,首先我们一起聊聊编程范式。

目前,最常见的编程范式是指令式(也称命令式)编程。我们学习编程,往往首先学习编程语言,学习每种语言的流程控制语句,学习每种语言自带的数据结构。通过组合这些流程控制指令和语言固有的数据结构,我们就能得到一个可执行自动化程序。我们往往只需使用 3 种流程控制指令即可满足编程需求:运算语句,循环语句,跳转语句(有条件的/无条件的分支语句)。这种指令式的编程范式脱胎于图灵机(一个有着纸带、读写头、寄存器、规则表的机器,就能解决任意 P/NP 问题),并且具备数学上的完备性。
image.png
相对指令式编程来说,响应式编程作为一种编程范式,已经不再囿于对于编程原理的概括和描述。响应式编程之所以产生,是为了表达对建立模型的抽象看法,与其说是编程所使用的一些思维方式,不如说是建立模型所使用的一些思维方式。我们如何描述事物特征,如何描述事情经过,这里必然有建立模型的一个过程。换而言之,先有场景,再有模型,再有响应式编程范式。

下面我们接着聊聊,在哪些场景下,我们可以自然的想到响应式编程模型。

应用场景

App 页面 / Web 网页交互事件

对于手机 App 或者 Web 前端开发者来说,响应式编程的思维方式可能造就习以为常,因为他们每天在做的就是这种事情。给页面的按钮点击事件绑定一个处理函数,这个函数就会响应用户点击页面按钮的行为,我们把用户点击事件视作一个 Observable(可观察对象),开发者编写的处理函数视作一个 Observer(观察者),那么这就是一个典型的响应式编程模型。

或者我们给 Web 前端路由绑定一个处理函数,这个函数就会响应前端路由变化,我们把路由变化事件视作一个 Observable(可观察对象),开发者编写的处理函数视作一个 Observer(观察者),那么这也是一个典型的响应式编程模型。

RESTful 外部服务调用

许多 RESTful 外部服务调用总是成群出现,并且存在进行下一个调用之前必须等待前一个调用完成,或者几个调用可以并发执行这些各种各样的调用关系。问题在于,调用超时导致整个调用链路不可用等等异常场景,需要我们进行细致处理。除此之外,处理这些调用关系,也会产生繁琐的面条代码逻辑。RESTful 外部服务调用,尤其是调用之间依赖关系的复杂编排,可以用响应式编程进行优化。

如果我们把外部服务看作 Observable(可观察对象),开发者编写的处理函数视作一个 Observable (观察者),那么这是一个典型的响应式编程模型。

我们声明每个外部服务调用有哪些状态需要响应,然后把状态的集合作为一个 Observable(可观察对象),开发者编写的处理函数视作一个 Observable(观察者),如此一来可以进一步的响应式的处理外部服务调用组合逻辑。当然我们顺着,处理之前处理结果,这种思路递归下去,我们就会得到 Stream (流) 的概念,这点稍后再表。

消息消费

消息消费天然也是一种响应式编程的使用场景,因为消息队列天然就能视作一个 Observable(可观察对象),开发者编写的对消息不同状态的处理函数就是一个 Observer(观察者),所以我们看到响应式编程的思路多么自然。

值得注意的是,顺着响应式编程的思路处理消息,我们可以解决多线程高并发问题。

通常来说,我们希望代码从上往下按照顺序一次一句执行。但是我们通过把代码并行执行,然后通过 Observer 以任意的顺序进行处理,如此可以充分利用现代计算机多线程计算的优势。另外一种的优化是,Observer 和 Observer 之前相互逻辑隔离,我们很容易把逻辑放入 Observer,然后不同的 Observer 放到不同的线程进行处理。此外,通过合理的划分 Observer 的粒度,把临界区资源放到一个 Observer 进行处理,非临界区资源放到其他 Observer 进行处理,我们甚至可以做到无锁编程。

总结

上述部分术语详细解释可以参见下文术语章节,总结一下这些场景,我们可以发现这些场景存在三个特征,第一,事件永远是由 Observable(可观察对象)推送给 Observer(观察者),这是一个典型的推模型。第二,事件可以视作键值数据而非关系型的数据,我们可以看到无论点击事件也好,消息推送事件也好,皆是如此。第三,上述场景存在大数据的可能性,比如消息推送事件。也就是说,推模型(pull)键值对(k/v)大数据(big data),构成了响应式编程的问题域。
image.png
同时,人们对于合格的响应式系统提出了对应的指标,并且形成《响应式宣言》,对于响应式系统的种种指标进行了详细说明。
自顶向下打开响应式编程 - 图5

发展历史

根据上述场景,人们通过不断实践,发展出了相应的响应式编程技术。响应式编程相关技术的发展历程,按照 David Karnok(RxJava 排名第一的开发者) 的观点可以划分 5 代。不过笔者看来 4 代、5 代其实只是技术升级,不能算作突破性的改动,所以略去不表。
image.png

原始时代:Observable Pattern

Observable Pattern (响应式编程设计模式)以及相关实践面世,包括 JavaScript Web 编程常见的 addEventListener,Java 服务器编程场景的 java.util.Observable API 等等。这种响应式编程设计模式相对后来的响应式编程范式,最大的区别在于缺乏可组合性(没有 Operator,没有 Stream),人们往往把响应式编程设计模式作为一部分异步问题的解决方案,但是不会考虑全流程使用响应式编程范式。

第 1 代:ReactiveX 1.0

来自微软的 Erik Meijer 及其后继的开发者发现单纯使用上述设计模式存在问题之后,进行了一系列尝试,包括 2010 年的 Rx.NET,2011 年的 Reactive4Java 以及 2012 年的 RxJava 早期版本,我们统称 ReactiveX 1.0。ReactiveX 1.0 技术包括很多突破性的工作,定义了响应式编程范性的问题域:大数据、推消息、键值对,使用 category theory(范畴论)duality(代数中的对偶)进行流程分析,得出下文将会涉及到的 Stream(流) 概念,并且通过 Stream 推导出了 Operator 等等概念。术语章节将会对此进行详细描述。
Erik 后来写了一篇很有趣的文章 《Your mouse is a database》,非常系统的介绍了关于这一代的 ReactiveX 所做出的努力。

第 2 代:ReactiveX 2.0

经过一段时间实践,人们发现 ReactiveX 1.0 技术存在一定问题,主要体现 ObserverObservable 使用同一线程,所以无法取消它们所对应的进行中的 Stream。除此之外,Observable 推送事件不会考虑 Observer 消费情况,在数据量大的情况下容易造成下游压力,也就是所谓的 Back pressure(背压)问题。
针对上述问题,ReactiveX 2.0 及其类似项目开始使用协程进行消息处理,并且增加 lift 通用方法保证每个 Operator 都会生成一个新的 Observable,从而 Observable 可以通过控制协程数量创建特定数量的消息,解决背压问题,而且每个 Operator 都能随时中断。

第 3 代:Reactive Streams

随着响应式编程范式的发展,越来越多的响应式编程框架开始涌现。为了能够得到一个更好的响应式编程生态环境,开发者们希望这些框架相互兼容,减少用户使用成本,于是主流框架的开发者一起讨论出了一个标准,称为 《Reactive Streams》。链接中的 Specification 章节详细描述了4 个定义:Publisher(同 Observable)、Subscriber(同 Observable)、Subscription(消息执行的上下文)以及 Processor(同 Operator),并且描述了它们对应的 43 条规则。

术语

Observable(可观察对象)

Observable(可观察对象)又被称为 Publisher(发布者),两个术语等价。
从本质上来说,Observable 是一个顾名思义的自然概念,可观察的对象。Observable 状态和信息的观察事件属于一种惰性事件

从形式上来说,如果事件在消费的时候才会产生并且推送,那么它们称为 Event(惰性事件,或者异步事件)。Observable 是一组惰性事件的集合。这里我们可以对比 Promise(来自 JavaScript 的概念,或者 Future,来自 Java 的概念),两者形式上的区别在于,Promise(或者 Future) 只会推送一个惰性事件,Observable 可以推送多个惰性事件。

我们可以发现有很多种类型的 Observable。一组自然数可以是 Observable,一个定时器可以是一个 Observable,一个延时器可以是一个 Observable,一个 RESTful 调用可以是一个 Observable,一个消息队列可以是一个 Observable

Observer(观察者)

Observer(观察者)又被称为 Subscriber(订阅者)Watcher(观察者)或者 Reactor(响应者),这些术语等价。

Observer 顾名思义是惰性消息的观察者,处理来自 Observable 的每个惰性消息。感性来说,Observable 往往是抽象概念,Observable 往往是需要开发者自行编写的处理逻辑,用于处理观察到的状态或者数据。

Stream(流)

如果只有 ObservableObserver,那么我们得到的仅仅是一种 Observer Pattern(观察者设计模式),并不能称为真正的 Reactive Programming(响应式编程)。下面我们将会聊聊 Stream 的概念,有了它的介入才是真正意义上的响应式编程。

我们知道,Observable 是一组惰性事件的集合。被 Observable 观察的时候,我们给某些惰性事件依次起名 自顶向下打开响应式编程 - 图7 。我们接着可以得到一个事件集合 自顶向下打开响应式编程 - 图8 ,这个事件集合可以称为 Stream(流)。
值得注意的是,这些事件包括 Event(普通事件),也包括 Error(错误)。一个 Stream 往往可以画成如下所示,横轴表示时序,圆点表示 Event,红叉表示 Error,这个图像形象的描述了一个 Observable 按照时间顺序向外推送 Event 的过程。
image.png
绘制 Stream 另一种方式是 ASCII 字符,上图可以用如下的 ASCII 字符表示:

  1. --a---b-c---d---X---|->
  2. a, b, c, d are emitted values
  3. X is an error
  4. | is the 'completed' signal
  5. ---> is the timeline

对于响应式编程的一种定义是,面向 Asynchronous data streams(异步数据流)的编程范式。看到这里大家可能感到困惑,我们这里不是只能得到 1 个 Stream,为什么出现了所谓的 Asynchronous data streams 呢?
通过 Operator(操作符) 处理 Stream,我们就能得到新的 Stream,这些 Stream 的集合就是 Asynchronous data streams。这里我们引入一个新的概念 Operator,下面我们将会进行详细解释说明。

Operator(操作符)

一个 Observer 自顶向下打开响应式编程 - 图10 处理 自顶向下打开响应式编程 - 图11 之后,接着创建新的惰性事件 自顶向下打开响应式编程 - 图12,那么这个对象既能作为前者的 Observer 又能看作后者的 Observable,可以使用 自顶向下打开响应式编程 - 图13 描述。这个对象 自顶向下打开响应式编程 - 图14 就被称为 Operator(操作符)。

Operator 可以通过一个函数表示,但是根据实际情况,常常存在上下文副作用,所以我们不能称之为 Pure function(纯函数)。比如某个 Operator 想要起到 debounce(节流)作用,那么首先惰性事件 自顶向下打开响应式编程 - 图15 除了必要状态信息,还要携带创建时间。Operator 也要保留一定时间间隔内的所有事件(上下文副作用),并且在时间间隔结束之后,取得最新一次事件发送。
image.png
如果有两个 Operator 分别是 自顶向下打开响应式编程 - 图17,以及自顶向下打开响应式编程 - 图18。那么我们可以组合两个 Operator 得到 自顶向下打开响应式编程 - 图19,也就是说,一个惰性事件 自顶向下打开响应式编程 - 图20,经过两个 Operator 处理之后,将会经过 自顶向下打开响应式编程 - 图21 得到中间惰性事件 自顶向下打开响应式编程 - 图22,经过 自顶向下打开响应式编程 - 图23 最终产生惰性事件 自顶向下打开响应式编程 - 图24。我们可以把两个 Operator 看作一个新的集合 自顶向下打开响应式编程 - 图25(前文提到的对偶思路体现在这里)进行考察,研究 Asynchronous data streams 集合和 Operator 集合的关系。

Operator 的抽象层次非常高,如果我们不用集合论/范畴论的观点进行审查,常常无法描述清楚,需要大量实践才能获得感性认识,这个就是响应式编程难以一言蔽之的原因。

Marble diagrams(大理石图)

刚才说到,我们通过响应式编程范式的一系列抽象,我们可以更加聚焦 OperatorAsynchronous data streams 之间的关系。为了更好的研究两者的关系,人们往往使用大理石图进行描述和分析。

image.png

大理石图分成三行,第一行是一个 Stream,第二行是 Operator,第三行是 Operator 转换后的 Stream。第一行和第三行的时间一一对应,所以很清晰的看到随着时间变化,Stream 被转换的情况。

结束语

有了上述基本的知识点,我们再来看看很多介绍响应式编程的文章就会变得非常轻松,希望这篇文章可以对你起到帮助。下面列出了一系列的参考资料,希望可以有所帮助。

参考资料

https://queue.acm.org/detail.cfm?id=2169076
https://akarnokd.blogspot.com/2016/03/operator-fusion-part-1.html
https://spring.io/blog/2016/06/07/notes-on-reactive-programming-part-i-the-reactive-landscape
https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
https://www.infoq.com/presentations/Simple-Made-Easy
https://reactivex.io
https://developer.ibm.com/articles/defining-the-term-reactive
https://www.reactivemanifesto.org/zh-CN