CSP

Communicating Sequential Processes | 通信顺序进程

CSP 代表"Communicating Sequential Processes",它既是一种技术,也是引入它的论文的名称。1978 年,Charles Antony Richard Hoare 在计算机械协会(更通俗地称为 ACM)上发表了这篇论文。在该论文中,Hoare 认为输入和输出是两个被忽视的编程原语,特别是在并发代码中。在 Hoare 撰写本文时,关于如何构造程序的研究仍在进行中,但大部分工作都是针对连续代码的技术:goto 语句的使用正在讨论中,面向对象的思想开始萌发。并发并没有得到太多关注。Hoare 开始纠正这个问题,于是他的论文和 CSP 诞生了。

在 1978 年的论文中,CSP 只是一个简单的编程语言,仅仅是为了展示顺序过程的交流能力; 实际上,他甚至在论文中说过:

因此,本论文中介绍的概念和符号……不应被视为适合用作编程语言,无论是抽象的还是具体的编程语言。

Hoare 非常担心他所提供的技术没有进一步研究程序的正确性,而且这些技术可能不是以他自己的真实语言来表达的。在接下来的六年里,CSP 的概念被提炼成一种被称为过程演算的形式化表示,以便采取交流顺序过程的思想,并实际开始推理程序的正确性。过程演算是数学建模并发系统的一种方式,也提供了代数法则来对这些系统进行转换,以分析它们的各种属性,例如效率和正确性。虽然过程计算本身就是一个有趣的话题,但它们超出了本书的范围。而且由于关于 CSP 的原始文件和从其演变而来的语言在很大程度上是 Go 的并发模型的灵感,所以我们将重点关注这些。

为了支持他的观点,Hoare 的 CSP 程序设计语言包含原型来正确模拟输入和输出或进程之间的通信。Hoare 将术语"进程"应用于逻辑的所有封装部分,这些部分需要输入来运行并产生其他过程将消耗的输出。当他写论文时,Hoare 可能用“功能”这个词来描述如何构建社区中的程序。

为了进行流程之间的沟通,Hoare 创建了输入和输出命令 ! 用于将输入发送到进程中,以及?用于读取进程的输出。每个命令都必须指定一个输出变量(在从流程中读取变量的情况下)或目标(在将输入发送到进程的情况下)。有时候这两个过程会引用相同的东西,在这种情况下,这两个过程将被认为是相对应的。换句话说,一个进程的输出将直接流入另一个进程的输入。下表给出了几个例子。

表达式 说明
cardreader?card image 从 cardreader 读取卡并将其值(字符数组)分配给变量 cardimage
lineprinter!line image 对于 lineprinter,发送 lineimage 的值进行打印
X?(x, y) 从名为 X 的进程中,输入一对值并将它们分配给 x 和 y
DIV!(3*a+b, 13) 从进程 DIV 输出 2 个指定的值
*[c:character; west?c → east!c] 从 west 读取所有字符并逐个放入 east

Go 的通道与之相似之处很明显。注意表格的最后一个例子中,来自 west 的输出是如何发送给变量 c 的,并且输入为 east 的输入来自同一个变量,这两个过程相对应。在 Hoare 关于 CSP 的第一篇论文中,进程只能通过指定的来源和目的地进行通信。他承认,这会导致代码作为库的嵌入问题,因为代码的使用者必须知道输入和输出的名称。他同时提到注册所谓的“端口名称”的可能性,其中该名称可以在并行命令的头部声明,我们可能会将其命名为命名参数并命名为返回值。

该语言还利用了所谓的守护命令,Edgar Dijkstra 在 1974 年撰写的一篇文章“Guarded commands, nondeterminacy and formal derivation of programs”中介绍了这一命令。守护命令由 → 分割。左侧是右测的有条件的守护,如果左测是错误的,或者在命令的情况下返回假或退出,则右测永远不会执行。将这些与 Hoare 的 I/O 命令结合起来为 Hoare 的通信进程奠定了基础,从而为 Go 的通道奠定了基础。

通过使用这些原语,Hoare 演示了几个例子,并演示了一种支持通信建模的语言如何使解决问题变得更简单,更容易理解。他使用的一些符号有点简单(perl 程序员可能不同意),但他提出的问题有非常明确的解决方案。Go 中的类似解决方案稍长一些,但也带有这种清晰度。

历史证明了 Hoare 是正确的; 然而,有趣的是,在 Go 发布之前,很少有语言确实将这些原语支持到语言中。大多数流行的语言都倾向于共享和同步对 CSP 的信息传递风格的访问。也有例外,但不幸的是这些仅限于没有广泛采用的语言。Go 是第一批将 CSP 原理融入其核心的语言之一,并将这种并发编程风格带给了大众。它的成功使得其他语言也试图添加这些原语。

内存访问同步本质上并不坏。我们将在后面的章节中(在“Go 的并发哲学”一节)中指出,有时在某些情况下共享内存是合适的,即使在 Go 中也是如此。但是,共享内存模型可能难以正确使用——特别是在大型或复杂的程序中。正是因为这个原因,并发才被认为是 Go 的优势之一:它从一开始就以 CSP 的原则为基础,因此很容易阅读,编写和推理。