众所周知,我是π的支持者。原因很简单,就是π对“并发”这个概念有一个极简而完整的表达。对于程序员,我不建议系统地去学习各种过程微积分或者过程代数。从根本上说,这是一个基于符号重写的逻辑系统,与编程关系不大。而且,对逻辑基础的要求太高了。但如果想一窥大师的思路,推荐阅读罗宾·米尔纳的图灵奖演讲:Elementsofinteraction,网上有,很容易读懂;我相信它将共享变量视为进程并将通信分开。程一和O两部分的想法大家都能理解,也很有启发。本文是对RP协议原型实现的反思。RP协议是一种类似于HTTPRestful的通信协议,但不依赖于HTTP。某种程度上,它更喜欢像网络文件系统一样完成双方的交互,但在操作语义上,它选择了Restful资源模型,而不是文件系统。打开/读/写/关闭流语义。在π中,最基本的一种表达方式是输入前缀的形式,c(x).P,表示P在通道c收到x消息后才会开始运行,非常接近事件驱动模式。实际上,事件和消息之间没有真正的区别,两者在数学上是相同的。但是这里需要注意的是,一旦x出现,这个表达式就被求值了,或者说,当reduction发生的时候,它就不存在了,相当于一个数学表达式的计算。这和另外一种情况不一样,像一个库函数,或者一个服务点,函数可以被调用无数次,服务点可以服务无数次,而不是调用一次或服务一次就消失;这时,在π这里,用!表示,这个符号叫做replication,意思是它后面有无数个表达式,每次reduce之后都可以继续使用。复制在编程中广泛存在。如果一个功能只能使用一次,大家会认为它没用。但是一个function或者一个servicepoint是一次性消费的singleton(以下简称sink)还是replication,这里有很大的区别。让我们举个例子。在RP协议中,双方的所有通信都是通过MessagePassing完成的;在这种情况下,如果要模拟一个简单的Request/Response:当Client发送消息时,必须立即创建一个Path,也就是名字,或者Speakingofchannel,π意义上的channel,都是π中的名字是频道;它发送的消息可能是这样的:{to:'/hello',from:'/requests/123-456-789',//临时创建的方法:'GET'}在服务器端。显然,/hello是复制而不是接收器。如果你在没有方法属性的情况下向这个Path发送消息,它不知道如何处理它;换句话说,/hello只是复制。client'sfrom,这个资源很有意思,先是sink,这个id123-456-789应该也是一次性的,client立即分配,应该避免冲突,就像TCP的ephemeralport一样;当前向服务端发送消息时,与异步π逻辑相同,即客户端向服务端发送操作请求,同时给它一个名称/通道(from),服务器可以将结果发送到这个名称/通道,以便“模拟”一个异步函数或RPC过程。这个from路径可以是一次性的,返回后这条路径会“消失”,就像前面说的归约后表达式消失一样;因为它接受像输入前缀这样的消息,所以被称为Sink;Sink的反义词是Source,后面会遇到。from也可以是复制。比如可以接受GET,返回请求的参数。它可能没有功能意义。如果只是一次性的Sink,那么生命周期仅限于一个请求/响应消息来回的过程。但它的生命周期可能会更长。例如,服务器返回的不是JSON对象,而是Stream、ObjectStream或BinaryChunkStream,或者混合,这取决于协议是如何设计的。此时from路径代表的资源是一个StreamSink,即可以接收多个Message,它需要表达的信息要丰富得多,比如进度,速度等等。这时候它也具备了复制的服务能力,接受Restful方法操作就更有意义了。更复杂情况的例子。例如,一个HTTPPost/Put/Patch需要上传一个流。我们会发现HTTP的设计并不是原子的。它实际上是在内部发出请求,等待服务器回复100Continue,然后继续上传内容。在RP中,这些语义被扁平化了。如果Post/Put/Patch需要上传流,服务器会立即返回一个即时创建的SinkPath。例如:客户端请求:{to:'/files',from:'/requests/123-456-789',method:'POST',...}服务器响应:{to:'/requests/123-456-789',status:100,sink:'/files/#/sinks/3223a6f3'}在服务器的应答中,立即分配一个sink;之后客户端就可以一个接一个的向这个sink发送消息,形成一个stream。显式创建Sink标识符,一方面可以简化路由,另一方面更符合π的输入前缀设计,即使用独立的名称/通道完成独立的计算任务;在流的意义上,这个输入前缀可以多次使用,直到遇到EOF/Null-terminator后消失。这里有趣的是,客户端的/requests/123-456-789是这个流的源标识符。它也必须是一个复制品,为什么?因为在实际使用中难免会需要取消流量或流量控制,服务端可以直接考虑操作这个资源,例如:{to:`/requests/123-456-789`,method:'PATCH',body:{flow:false}}相当于暂停流程。如果流暂停。在一个好的实现中,应用层应该将请求实现为stream.Writable,这样应用层在继续写入流之前应该考虑drain事件的发生。这是什么?这是π中的输出前缀,对吧?消息的发送可以阻止进程继续执行,直到消息被发送。虽然在π中很常见没有buffer,但是输出前缀和输入前缀直接归约成一个新的表达式;在实际程序中,总是会用到buffer,但是buffer的空间也一定是有限的,最终输出前缀会发生在buffer满了之后。异步编程和面向通信的编程肯定是所谓的惰性,但是在很多语言中实现的是生产者消费者模式,需要重类和模式;恕我直言,在一个很好地支持并发/异步的语言中,这个越轻越好,node.js中的发射器、流和回调都是良好设计的例子。总结:区分Sink和Replication很重要,这是两个完全不同的职责;Sink不接受带有动词的消息;复制只接受带有动词的消息;资源标识符只能具有其中一个职责,或两者兼有;Stream的Source/Sink都需要一个独立且唯一的资源标识,更能体现π中name/channel的含义;它还大大简化了编程和错误处理。比如Sink端已经放弃了Stream,而Source端还在发送。你会发现,如果不独立建立一个StreamSink,处理起来并不容易,健壮且高效;在设计理念上,单向消息传递是底层;Request/Response、Stream、StreamControl都是上层;但不管是哪个Layer和基于路径的资源标识都是可用的,因为在π中,一切都是名字,所有的名字都是通道。这才是Restful中URI的真正含义,在RP中更是发挥到了极致。AlanKay直观地解释了OO的本质是MessagePassing,而RobinMilner在π中给出了一个最小化的数学定义。我不认为我能比这两位图灵奖获得者加起来更聪明。就接受圣贤的概念。希望没有误会。
