Go语言中的并发机制系列-2|Go的并发哲学 & CSP

Posted by ShenHengheng on 2018-11-25

本系列阅读笔记是基于 Golang 经典图书 Concurrency in GO ,主要有6个章节:

  • An Introduction to Concurrency
  • Modeling Your Code: Communicating Sequential Processes
  • Go’s Concurrency Building Blocks
  • Concurrency Patterns in Go
  • Concurrency at Scale
  • Goroutines and the Go Runtime

本部分主要介绍第二章:Modeling Your Code: Communicating Sequential Processes ,Go并发设计哲学。

并发与并行的区别

Concurrency is a property of the code; parallelism is a property of the running program.

这揭示了一些有趣而重要的事情。

  • 第一个有趣的事情是,我们不写并行代码,只写我们希望并行运行的并发代码。并行性是我们程序运行时的属性,而不是代码。
  • 第二个有趣的事情是,我们发现有可能 - 甚至可能 - 不知道我们的并发代码是否实际并行运行。这只能通过我们程序模型下面的抽象层 “并发原语” 来实现,程序运行时,操作系统、操作系统运行的平台(包括在虚拟机管理程序,容器和虚拟机的情况下),最终是CPU。这些抽象使我们能够区分并发性和并行性。
  • 第三个也是最后一个有趣的事情是,并行性是时间或上下文的函数。还记得在 “原子性” 中我们讨论过语境的概念吗?在那里,上下文被定义为操作被视为原子的边界。这里,它被定义为可以将两个或多个操作视为并行的边界。

例如,如果我们的上下文被分成5个单位时间片,并且我们运行了两个操作,每个操作花费一秒钟来运行,我们会认为操作是并行运行的。如果我们的上下文是一秒钟,我们会认为操作是按顺序运行的。

CSP

当人们讨论 Go 的时候,往往会谈到 Go 语言的成功离不开 CSP。那么CSP是什么?

CSP代表“通信顺序进程”,它既是一种技术,也是引入它的论文的名称。1978年,Charles Antony Richard Hoare 在 ACM 上发表了这篇论文

在这篇论文中,Hoare认为输入和输出是两个被忽略的编程原语,特别是在并发代码中。在Hoare撰写该论文时,关于如何构建程序的研究仍在进行中,但大部分工作都是针对顺序代码的技术: goto 语句的使用正在争论中,面向对象的范式正在开始扎根。同时 进程 的操作没有经过深思熟虑。Hoare 开始纠正这个问题,因此他的论文和CSP诞生了。

在1978年的论文中,CSP只是一种简单的编程语言,仅用于展示 CSP 的能力;事实上,他在论文中提到:

Thus the concepts and notations introduced in this paper should … not be regarded as suitable for use as a programming language, either for abstract or for concrete programming.

Hoare 非常担心他所提出的技术对于进一步研究程序的正确性没有任何帮助,并且这些技术可能无法在基于他自己的真实语言中表现出来。在接下来的六年中,CSP的概念被细化为一种称为 process calculus 的正式表示,以便采用 CSP 的思想并实际开始推理程序的正确性。

过程演算是一种对并发系统进行数学建模的方法,并且还提供代数法则来对这些系统执行变换以分析它们的各种属性,例如效率和正确性。

为了支持输入和输出需要被视为原语的断言,Hoare 的 CSP 编程语言包含了正确地模拟进程之间的输入和输出或通信的原语。Hoare将术语进程应用于逻辑的任何封装部分,这些逻辑需要输入运行并产生其他进程将消耗的输出。Hoare可能会使用“函数”这个词,如果不是在撰写论文时如何构建社区中发生的程序的辩论。

对于进程之间的通信,Hoare创建了输入和输出命令:! 用于将输入发送到进程中,用于读取进程的输出。每个命令都必须指定输出变量(在从进程读取变量的情况下)或目标(在向进程发送输入的情况下)。有时这两个会引用相同的东西,在这种情况下,两个过程将被认为是对应的。换句话说,来自一个进程的输出将直接流入另一个进程的输入。表1显示了该论文的一些例子。

Operation Explanation
cardreader? cardimage From cardreader, read a card and assign its value (an array of characters) to the variable cardimage.
lineprinter! lineimage To lineprinter, send the value of lineimage for printing.
X?(x, y) From process named X, input a pair of values and assign them to x and
y.
DIV!(3*a+b, 13) To process DIV, output the two specified values.
→ east!c] east. The repetition terminates when the process west terminates.

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

该语言还使用了所谓的守卫命令( guarded ),Edgar Dijkstra 在 1974 年写的一篇文章 “Guarded commands, nondeterminacy and formal derivation of programs”中提到了:一个守卫的命令只是一个左右手边的声明,用 分开。箭头左边作为右边的条件或守卫,如果左边是假的,或者在命令的情况下,返回假或退出,右边将永远不会被执行。将这些与 Hoare 的 I/O 命令相结合,为 Hoare 的通信过程奠定了基础,从而为 Go 的Channel 奠定了基础。

内存同步访问本质上并不坏。我们将在“Go的并发哲学”中看到,有时共享内存在某些情况下是合适的,即使在Go中也是如此。但是,共享内存模型可能难以正确使用 - 尤其是在大型复杂程序中。正是由于这个原因,并发被认为是Go的优势之一:它从一开始就是基于CSP的原则而构建的,因此它易于阅读,编写和推理。

Go的并发设计哲学

CSP曾经并且是Go设计的重要组成部分;但 Go 还支持更传统的通过内存同步访问编写并发代码的方法以及遵循该技术的原语。sync 和其他包中的结构和方法允许您执行加锁,创建资源池以及抢占 goroutine 等。

这种在CSP原语和内存同步访问之间进行选择的能力对程序员来说非常有用,因为它可以让程序员更好地控制选择写入什么样的并发代码来解决问题,但它也可能有点令人困惑。Go 语言的新手常常会得到这样的印象: CSP 的并发风格被认为是在 Go 中编写并发代码的唯一方法。例如,在同步包的文档中:

Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.

language FAQ

Regarding mutexes, the sync package implements them, but we hope Go programming style will encourage people to try higher-level techniques. In particular, consider structuring your program so that only one goroutine at a time is ever responsible for a particular piece of data. Do not communicate by sharing memory. Instead, share memory by communicating.

还有许多文章,讲座和访谈,其中Go核心团队的各个成员支持 CSP 样式而不是像 sync.Mutex 这样的原语。

因此,完全可以理解为什么Go团队选择公开内存访问同步原语。但令人困惑的是,你会看到频繁出现的同步原语,看到人们抱怨过度使用频道,还听到一些Go团队成员说可以使用它们。以下是关于此事的 Go Wiki 的引用:

One of Go’s mottos is “Share memory by communicating, don’t communicate by sharing memory.” That said, Go does provide traditional locking mechanisms in the sync package. Most locking issues can be solved using either channels or traditional locks. So which should you use? Use whichever is most expressive and/or most simple.

Go的一个主题是“通过通信共享内存,不通过共享内存进行通信。”

也就是说,Go确实在同步包中提供了传统的锁定机制。

使用通道或传统锁可以解决大多数加锁问题。

那应该用哪个?使用最具表现力和/或最简单的表达方式。

决策树

决策树

总结

本节主要讲解了Go的并发哲学,并且为什么Go的并发那么优秀:基于CSP原则,然后讲了CSP是什么和它的设计原则,最后说明如何选择 “通道” 还是 “传统锁”!