当讨论Go时,你会经常听到人们围绕CSP进行争论。它会被称为Go成功的原因,或者是并发编程的灵丹妙药。虽然CSP使事情变得更容易,而且程序更加强大,但不幸的是这不是一个奇迹。 那它是什么?
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的原则为基础,因此很容易阅读,编写和推理。