众所周知我是π的支持者。缘由很简单,就是π有着对『并发』概念的最小化完备表述。node
对于程序员来讲我不推荐系统学习各类进程演算或者进程代数,根本上这是基于符号重写的逻辑系统,跟编程的关系不大并且,对逻辑学基础的要求过高了。程序员
但若是要一窥大师思想,我推荐阅读Robin Milner的图灵奖演讲:Elements of interaction,这个网上一搜就有,并且很好读;并且我相信它里面把共享变量看做进程,把通信拆成I和O两个部分的思想,都是你们能够看懂的,并且,是醍醐灌顶的。编程
本文是在实现RP协议的原型时的一个思考。RP协议是一个相似HTTP Restful的通信协议,可是它不依赖HTTP,在必定程度上它更但愿象网络文件系统那样完成通信双方的交互,但操做语义上选择了Restful的资源模型,而不是文件系统的open/read/write/close流语义。网络
在π里,最基础的表达式之一是input prefix,c(x).P
这样一个形式,它的意思是P
要等到channel c收到一个x消息才开始运行,这很是接近事件驱动模式,实际上,事件和消息并无真正的区别,在数学上都同样。并发
可是这里须要注意的是,一旦x出现,这个表达式就被估值了,或者说,发生reduction,它就再也不存在了,至关于一个数学表达式计算完了。异步
这和另外一种状况不同,就像一个库函数,或者一个服务点,函数能够被调用无数次,服务点能够服务无数次,而不是调用一次或者服务一次就消失了;这个时候,在π里,是用!
来表达的,这个符号叫作replication,意思是它后面的表达式有无数个,每次reduce一个以后还能够继续使用。异步编程
replication在编程中是普遍存在的,若是一个函数只能用一次你们都会以为它没什么用了。函数
可是一个函数或者一个服务点是一次性消费掉的单例(如下称为sink),仍是replication,这里区别很大。咱们举个例子。学习
在RP协议里,通信双方的全部通信都是经过Message Passing完成的;在这种状况下若是想模拟一个简单的Request/Response:设计
Client在发出消息的时候要即时建立一个Path,也就是name,或者说channel,π意义上的channel,在π里一切name都是channel;它发出的消息多是这样的:
{ to: '/hello', from: '/requests/123-456-789', // 临时建立的 method: 'GET' }
在Server这端,很明显,/hello
是一个replication,并且不是一个sink
,若是你往这个Path发送一个message,没有method属性,它不知道怎么处理;换句话说,/hello
是replication only的。
Client的from
,这个资源颇有意思,它首先是一个sink,这个id 123-456-789也应该是一次性的,客户端即时分配它并且应该避免冲突,就像TCP的ephemeral port;
当前面的这个消息发送给Server时,它和异步的π逻辑是同样的,就是Client发送给Server一个操做请求,同时给了它一个name/channel (from
),服务端能够向这个name/channel发送结果,这样就『模拟』了一个异步函数或者RPC过程。
这个from
路径能够是一次性的,在返回以后这个路径就『消失』了,和前面说的reduction以后表达式消失了同样;由于它象input prefix同样是接受消息的,因此称为Sink;Sink的反义词是Source,后面会遇到。
from
能够同时是一个replication
。好比能够接受GET
,返回一下请求的参数之类。可能功能上没什么意义,若是它仅仅是一个一次性的Sink,生命周期仅限于一个request/response消息来回的过程。
可是它的生命周期多是更长的,好比,服务端返回的并非一个JSON对象,而是一个Stream,一个Object Stream或者一个Binary Chunk Stream,或者混合,看协议怎么设计。
这个时候这个from
路径表示的资源就是一个Stream Sink了,便可以接收多个Message,那它须要表述的信息就丰富不少,好比能够有进度、速度等等。这个时候它兼有replication的服务能力,接受Restful method操做就更有意义。
更复杂的状况例子。
好比一个HTTP Post/Put/Patch,是须要upload一个stream的。
咱们会发现HTTP的设计不是原子化的,它的内部其实是先发出请求,等待了Server端应答100 Continue,而后继续上传内容的。
在RP里,这些语义被扁平化了。Post/Put/Patch若是须要上传流,Server端马上返回一个即时建立的Sink Path。好比:
Client请求:
{ to: '/files', from: '/requests/123-456-789', method: 'POST', ... }
Server回答:
{ to: '/requests/123-456-789', status: 100, sink: '/files/#/sinks/3223a6f3' }
在Server的回答中,它即时分配了一个sink;以后客户端能够陆续向这个sink发送消息,构成一个stream。
显式建立一个Sink标识,一方面能够简化routing,另外一方面,它更符合π的input prefix设计,即便用一个独立的name/channel完成一个独立计算任务;从流的意义上来讲,这个input prefix可使用屡次,直到遇到EOF/Null-terminator后消失。
这里有趣的地方是,在Client一端的/requests/123-456-789
,它是这个stream的Source标识。
它必须同时是一个replication,WHY?由于实际使用中必然会有取消流或者流控的需求,那服务端直接考虑操做这个资源就能够了,好比:
{ to: `/requests/123-456-789`, method: 'PATCH', body: { flow: false } }
至关于暂停这个流。
若是这个流暂停了。在一个良好的实现下,在应用层应该相似把request实现成了stream.Writable
,这样在应用层应该考虑出现drain
事件时才会继续向stream写入。
这是什么?这就是π里的output prefix,对吧,消息的发出能够block一个进程继续执行,直到消息发出为止。
虽然在π里常见的是没有buffer的状况,output prefix和input prefix直接reduce成一个新的表达式;而在实际程序中,buffer老是要用到的,但buffer也必定是有限空间的,最终buffer满了后output prefix会发生的。
异步编程和面向通信的编程必定是这样的所谓Lazy的,只是在不少语言里实用producer-consumer模式实现,须要用很重的类和pattern;IMHO,在一个良好支持并发/异步的语言中,这个东西越轻越好,node.js里的emitter, stream, callback都是良好设计的典范。
总结:
设计哲学上,单向的Message Passing是底层;Request/Response,Stream,Stream Control,都是上层;但不管哪一层,基于路径的资源标识都是可用的,由于在π里,一切皆name,一切name皆channel,这就是Restful里的URI的真正含义,在RP里发挥到极致。
Alan Kay在直觉上解释了OO的本质是Message Passing,而Robin Milner在π里给出了最小化的数学定义,我以为本身不可能比两个图灵奖得到者加起来更聪明了,就躺倒接受先贤先圣的理念就能够了,但愿没有误读。