Go语言CSP:通信顺序进程简述

当进行和 Go语言有关讨论的时候,经常听到人们抛出 CSP 这个缩写。在某些环境下 CSP 经常被赞美成 Go语言成功的原因以及并发编程的“万能钥匙”。它让不知道 CSP 的人开始认为计算机科学已经发现了一些可以像变魔术一样的方法让编写一个并发程序像编写一个串行程序一样简单。

虽然 CSP 确实使这些变得更加简单,让程序变得更加健壮,但不幸的是它并不是一个奇迹。所以,CSP 到底是什么?为什么把大家都弄的如此兴奋?

CSP 即“Communicating Sequential Processes”(通信顺序进程),既是一个技术名词,也是介绍这种技术的论文的名字。在 1978 年,Charles Antony Richard Hoare 在 Association for Computing Machinery(一般被称作 ACM)中发表的论文。

在这篇论文里,Hoare 认为输入与输出是两个被忽略的编程原语,尤其是在并发代码中。在 Hoare 写作这篇论文的同时,关于如何架构程序的相关研究还在进行中,但是大部分的研究都是针对编写顺序代码的方能:goto 语句的使用正在被讨论,面向对象范型正在成为编程的基石。

并发操作并没有被给予过多的思考。Hoare 开始纠正这个现象,所以,关于 CSP 的这篇论文就横空出世了。

在 1978 年的论文中,CSP 仅是一个完全用来展示通信顺序进程的能力的一个简单的编程语言。事实上,他甚至在论文中写道:因此,本文介绍的概念和符号应该······不被认为适合作为一种编程语言,无论是抽象的还是具体的编程。

Hoare 深深的忧虑所展示的技术对未来的关于并发编程的研究没有任何作用,这种技术也许没有语言会真的按照他的想怯来实现。在接下来的 6 年里,关于 CSP 的想法被提炼成了一个叫做“进程微积分”的正式名称来将 CSP 的想也投入到并发编程的实践。

进程微积分是一种对并发系统进行数学化建模的方式,并且提供了代数也则来进行这些系统的变换来分析它们不同的属性,例如:并发与效率。而且正是因为 CSP 的原始论文以及从论文中进化而来的原语正是 Go语言并发模型的主要灵感,而这正是我们接下来所要聚焦的。

用来支撑他关于输入与输出需要被按照语言的原话来考虑,Hoare 的 CSP 编程语言包含用来建模输入与输出,或者说“在进程间正确通信”(这就是论文名字的由来)的原语。Hoare 将进程这个术语运用到任何包含需要输入来运行且产生其他进程将会消费的输出的逻辑片段。Hoare 可能应该使用“函数”这个词汇,而不是在他写论文时在社区中关于如何构建程序的辩论。

为了在进程之间进行通信,Hoare 创造了输入与输出的命令:! 代表发送输入到一个进程,? 代表读取一个进程的输出。每一个指令都需要指定具体是一个输出变量(从一个进程中读取一个变量的情况),还是一个目的地(将输入发送到一个进程的情况)。

有时,这两种方怯会引用相同的东西,在这种情况下,这两个过程会被认为是相对应的。换言之,一个进程的输出应该直接流向另一个进程的输入。下表给我们展示了 Hoare 的 CSP 论文申一些例子的摘录。

操作 解释
cardreader?card image 从 cardreader 中读取一条记录,并将它的值(一个数组)赋给变量 cardimage
lineprinter!line image 向 lineprinter 发送 lineimage 的值
x?(x, y)  从名为 X 的进程输入一对值,并将它们分配给 x 和 y
DIV!(3*a+b,13)  处理 DIV,输出两个指定的值
*[c:character; 读取所有 west 输出的字符,然后输出到 east。当过程 west 结束时
west?c → east!c] 终止

示例与 Go语言的 channel 的相似性是显而易见的。注意最后一个示例中 west 的输出被送到一个变量 c,然后 east 的输入也是从相同的变量中接收的。这两个过程是相同的。

在 Hoare 关于 CSP 的第一篇论文中,进程只能通过命名的源与目的进行通信。他承认这会让代码像一个函数库一样被嵌入到逻辑中,因为这段代码的消费者必须知道输入与输出的命名。他随意地提出将输入输出的名字注册成他称作“端口名”的可能性,也就是说,在并行命令的头部,需要声明名字,也就是我们大概可以认为是命名的参数与命名的返回值。

这种语言同时利用了一个所谓的守护命令,也就是 Edgar Dijkstra 在一篇之前在 1974 年所写的论文中介绍的,“Guarded commands, nondeterminacy and formal derivation of programs”。一个有守护的命令仅仅是一个带有左和右倾向的语句,由 → 来分割。

左侧服务是有运行条件的,或者是守护右侧服务,如果左侧服务运行失败,或者在一个命令执行后,返回 false 或者退出,右侧服务永远不会被执行。将这些与 Hoare 的 I/O 命令组合起来,为 Hoare 的通信过程奠定了基础,从而实现了 channel。

使用这些原语, Hoare 运行了几个示例,并演示了如何以最佳的方式支持建模通信,从而使解决问题变得更简单、更容易理解。他使用的一些符号是简短的(Perl 程序员可能不同意),并且提出的问题有非常清晰的解决方案。类似的解决方案比较长一些,但也很清晰。

经验判断 Hoare 的建议是正确的,然而,有趣的是,在 Go语言发布之前,很少有语言能够真正地为这些原语提供支持。大多数流行的语言都支持共享和内存访问同步到 CSP 的消息传递样式。

当然也有例外,但不幸的是,这些都局限于没有广泛采用的语言。Go语言是最早将 CSP 的原则纳入其核心的语言之一,并将这种并发编程风格引人到大众中。它的成功也使得其他语言尝试添加这些原语。

在 Go语言中,甚至有时共享内存在某些情况下是合适的。但是,共享内存模型很难正确地使用,特别是在大型或复杂的程序中。正是由于这个原因,并发被认为是 Go语言的优势之一,它从一开始就建立在 CSP 的原则之上,因此很容易阅读、编写和推理。