这篇文章的标题是一个π表达式,结尾是一段JavaScript代码,和这个表达式的含义彻底一致,或者说,完成了这个表达式的估值。express
π演算(π calculus)是一种表达并发过程(process)的数学语言,和λ在形式上有不少相似之处;λ已是公认的计算模型,它和图灵机或递归理论(Recursion Theory)描述的计算模型是等价的,可是这个计算模型没法描述并发计算。编程
π没有λ那样的地位;它只是描述并发计算的模型之一,在计算数学领域科学家们尚未对并发计算模型达成一致,每一种并发计算模型都强调了并发计算的某些方面的特性但尚未一个模型成为λ那样的经典模型能够解释其余全部模型,或者证实其等价性;数学家们选择使用不一样的模型研究他们感兴趣的并发计算特性。promise
π是并发计算模型中最精简的一个,具备最小的概念元素集;它只包含两个概念:过程(process)和通信(channel)。过程是一个计算单元,计算是经过通信来完成的。并发
“计算是经过通信完成的”对于不熟悉数学理论的人来讲不容易理解;这里也没有更好的解释;推荐看一点关于λ的介绍,感觉一下计算是如何经过变量名binding和替换(substitution)完成的。在数学上,λ能够被encode在π里,可是不应对encode一词产生基于其天然语言含义的联想,若是想了解它的确切含义,请系统学习λ和π的相关理论。dom
题目是一个π表达式,也能够看做是用π演算写下的一段程序,即把π看成一种编程语言,虽然这个表达式没有什么实际用处,也没有人会打算用π做为编程语言写下实际的程序;可是在对照了题目的π表达式和文中的JavaScript代码以后,我相信你会得到一些新感觉,关于对并发的理解,什么是描述并发的最小概念集。async
这篇文章是一个起点,若是我有时间,还会逐步解释在JavaScript里的callback,emitter,promise,async/await,等等,都不是在并发计算模型意义上的最小概念,它们更多的考虑了实际使用中最经常使用需求、最简洁书写、最容易学习等工程特性或需求,但同时也会遇到一些棘手的问题;而这篇文章,就是在探索对并发编程而言,最原始(primitive
)的东西是什么。编程语言
和λ同样简单的是,在π里只有name,name表示一个channel。ide
标题里的π表达式(π-term)没有包含全部的π符号,只包含了其中的一部分;解释以下:函数
'.'
(dot)能够被理解为继续(continuation),或者反过来理解,阻塞(blocking);它的意思是前缀必须先完成通信,即接收或发送完成,'.'
以后的表达式才能够开始执行,或者称为估值;这是π里惟一表达顺序(order)的地方;你能够给一个表达式取一个名字,习惯上使用大写字母P, Q, R...学习
例如:
若是 P = z<a>.0,最左侧的表达式就能够写成x(z).P;
若是 P = x(z).z<a>.0,则最左侧的表达式就能够写成P;
若是
- P = x(z).z<a>.0
- Q = x<w>.y<w>.0
- R = y(v).v(u).0
则整个标题的表达式能够写成 P | Q | R
有时候咱们采用π.P的写法表示一个通用的π表达式,而不关心这个表达式里的π具体是那种前缀;
固然也能够定义: U = P | Q | R,它仍然是π表达式。
每一个π表达式都表达了一个过程。
'|'
(vertical pipe)在π里的含义是并发组合,能够看做过程的运算符;U = P | Q | R就能够理解为过程U是由三个过程并发组成的。
π里的另外一个组合过程的运算符是
'+'
,summation,咱们暂不介绍。
标题的表达式里还有一个符号0
,0
表示一个无行为的过程(inaction)。
这一段能够在看了后面的代码以后再回来对照理解。
Free name的含义和λ或编程语言里的定义一致;它是bound name的反义词;bind的意思和λ也是一致的(λx);
在π里有两个符号会bind name,标题里的表达式只出现了一个,即输入前缀,例如:x(z)。这很容易理解,在JavaScript代码里咱们经常使用listener函数接收消息:
emitter.on('data', data => { // do something with data })
这里的data
变量的scope就是在这个匿名函数内的,即bound name。一个过程P的Free name是它和外部产生行为交互的惟一方式。
这里是π process教材里的描述:
The free names of a process circumscribe its capabilities for action: for a name x, in order for P to send x, to send via x, or to receive via x, it must be that x ∈ fn(P). Thus in order for two processes to interact via a name, that name must occur free in both of them, in one case expressing a capability to send, and in the other a capability to receive.
from π calculus by Davide Sangiorgi and David Walker
译:
一个过程的free name决定了它的行为能力,对于过程P中的name x,若是P可以:
- 发送x
- 经过x发送其余name
- 经过x接收其余name
x必须是P的free name。因此若是两个过程须要经过一个name交互,这个name必须在两个过程当中都是free name,其中一方用于发送,另外一方用于接收。
这个词在编程界被用烂了。可是它的含义没有什么高大上的地方。一个数学公式的形式变换就是reduction,固然咱们正常状况下是但愿它越变越简洁的(因此叫reduce),除了你的阴险的数学老师会在出题时有个相反的邪恶目的。
π只有一个reduction:
x<y>.P | x(z).Q -> P | Q[y/z]
含义是y从channel x发送出去以后,P才能够继续执行;同时x(z)前缀收到了y,Q得以继续执行,此时Q里的全部z都要替换成y。
在编程中:
x(z).Q意味着若是x还没有收到数据,Q不能开始执行;这个input prefix在程序语言里很容易实现,就是常见的listener或者callback。
x<y>.P意味着只有y被读走,P才会开始执行;这相似lazy实现的stream,在数据没有被读走以前,不会再向buffer填充数据;在编程语言里实现这个表达式,和实现readable stream时,收到drain
事件才开始填充数据相似。
咱们先假定存在一个构造函数或工厂方法,能够构造一个channel对象;咱们先不回答channel如何构造,以及它内部是什么。
咱们要求channel对象有一对接口方法,按照π的逻辑应该叫作send和receive;
注意在π里咱们没有类型和值的概念,一切变量皆channel,写成代码就是一切变量皆channel对象,经过channel传递的变量也是channel,这是π系统的重要特性之一:pass channel via channel(由于它会让name突破一个scope使用)。
咱们首先发现这个表达式里的free name都得先声明(why?);x,y,w,a都声明成channel(a在这个例子中没有实际用于通信,能够是任何东西)。
第一段代码就是这个样子。
class Channel { // placeholder } const channel = () => new Channel() const x = channel() const y = channel() const w = channel() const a = channel()
'.'
(dot)所表达的继续,咱们能够用调用一个函数来实现;.0
,能够用调用空函数(() => {}
)表示;
第一个表达式:x(z).z<a>.0,能够这样写:
x.receive(z => z.send(a, () => {}))
receive方法形式上是提供一个函数f
做为参数,channel x在接收到值z的时候调用这个函数f(z)
;
第二个表达式:x<w>.y<w>.0,能够这样写:
x.send(w, () => y.send(w, () => {}))
注意这里要send成功以后才能继续而不是调用send后就继续,因此不能写成:
x.send(w) y.send(w)
最后一个表达式:y(v).v(u).0
y.receive(v => v.receive(u => (() => {})()))
到这里咱们写完了使用者代码;在使用的时候咱们也给Channel类的接口下了定义;若是你问哪一个表示并发的vertical pipe(|
)哪里去了?你想一下,我在文章的最后给出问题的答案。
在实现Channel
类以前咱们还要考虑一个顺序问题。
π里的reduction是两个并发过程之间发生的;在reduction的时候咱们要调用两个函数实现'.'
表示的继续,分别是发送者继续和接收者继续,咱们是否应该约定一个固定的顺序?
答案是不该该;对于这里写下的玩具代码咱们甚至故意加入了随机性,这才是并发的含义,并发过程之间木有固定执行顺序。
咱们先定义一个reduce函数;它的前提是send和receive两个方法都被调用过了;这里存在两种顺序可能性:若是receive先被调用了,f被保存下来直到send被调用,这和常见的listener没有区别;但π也容许反过来的顺序,send先被调用了,则c和f都被保存下来,等到receive调用的时候再使用,这就是π里的两个前缀会block后面的表达式估值的实现。
不管send和receive的实际调用顺序如何,咱们都但愿reduce能够随机执行sender和receiver提供的回调函数。
class Channel { reduce () { if (!this.sendF || !this.receiveF) return let rnd = Match.random() if (rnd >= 0.5) { this.sendF() this.receiveF(this.c) } else { this.receiveF(this.c) this.sendF() } } send (c, f) { this.c = c this.sendF = f this.reduce() } receive (f) { this.receiveF = f this.reduce() } }
写出reduce以后send和receive就是无脑代码了;在标题的表达式里每一个channel都只用了一次,因此咱们不用在这里纠结若是重复发送和接受的状况如何解决;各类参数检查和错误处理也先无论了,先跑起来试试。
最后全部的代码都在这里,加了一点打印信息,你能够运行起来感觉一下,也思考一下:
class Channel { constructor (name) { this.name = name } reduce () { if (!this.sendF || !this.receiveF) return console.log(`passing name ${this.c.name} via channel ${this.name}`) let rnd = Math.random() if (rnd >= 0.5) { this.sendF() this.receiveF(this.c) } else { this.receiveF(this.c) this.sendF() } } send (c, f) { console.log(`${this.name} sending ${c.name}`) this.c = c this.sendF = f this.reduce() } receive (f) { console.log(`${this.name} receiving`) this.receiveF = f this.reduce() } } const channel = name => new Channel(name) const x = channel('x') const y = channel('y') const w = channel('w') const a = channel('a') x.receive(z => z.send(a, () => console.log('term 1 over'))) x.send(w, () => y.send(w, () => console.log('term 2 over'))) y.receive(v => v.receive(u => (() => console.log(`term 3 over, received ${u.name} finally`))()))
为何vertical pipe表示的并发组合没了呢?由于连续执行上面代码段里最后三句的时候,就是并发了;必定要说语言上什么符号对应了'|'
的话,对于JavaScript就是;
号了;它原本在语言上是statement的顺序组合,在咱们这个代码里,就是并发组合了。