Go语言并发之道读后感 - 第二章
CSP (Communicating Sequential Processes)
我们可能听说过,通过通信去共享内存,Go pipeline 等,这一切的核心思想就是 CSP 理论。在这一章作者详尽的介绍了 CSP 与 Go 并发哲学的关系。
并发与并行的区别
并发属于代码,并行属于一个运行中的程序。
引用 Erlang 之父 Joe Armstrong 图
并发 = 两个队列一个咖啡机,两个队列的人交替使用咖啡机,可能会遇见如下情况:
A 队列被 B 队列小伙伴插队了,这就是条件竞争。
A 队列有一位英俊小伙和 B 队列一位女神并排,轮到他们俩的时候两人互看对方一眼,互让一步同时彬彬有礼的说了一句:你先。这个时候活锁出现了。
B 队列一位小伙伴,是代替其他朋友来打咖啡,他带了一箱子杯子,终于轮到他了。这时饥饿就出现了。
A 队列一位小伙伴喜欢摩卡,卡布奇诺混合咖啡,B 队列一位小伙伴喜欢拿铁,摩卡混合咖啡。当他们互相等待对方摩卡出杯的时候后,死锁来了。
并行 = 两个队列两台咖啡机。
编写正确的并发逻辑越难,越需要我们将很简单的并发原语组合起来使用。在Go 语言出现之前,大部分的主流编程语言都有一系列的抽象层。 如果你想写并发代码,你需要对你的程序按照线程同步以及内存访问同步来建模。如果你有一大堆需要并发建模的东西,而你的计算机又不能处理那么多的线程,就需要创建一个线程池,并将你的操作在线程池中复用。
Go 语言在这个联调中加入了新的一环:goroutine。另外,Go 语言从著名的计算机科学家 C.A.R. Hoare 那里借用了不少概念,并且给我们提供了新的原语来使用,即 channel。Go 语言并发原语的根基论文: C.A.R. Hoare 开创性的论文 “Communicating Sequential Processes”。
在 Go 语言中线程依旧存在,但是已经不需要我们去操心线程创建,复用,销毁,合并等操作,由 Go 语言本身完成。我们只需要使用更加简单的 goroutine 和 channel 即可。偶尔需要考虑一下共享内存的问题。
什么是 CSP
当进行和 Go 语言相关的讨论的时候,你经常会听到人们抛出 CSP。
CSP 是 “Communicating Sequential Processes" 的缩写,译为通信顺序进程。在 1978 年,C.A.R. Hoare 在国际计算机协会工作时发表的论文。
在这篇论文里,Hoare 认为输入与输出时两个被忽略的编程原语,尤其是在并发代码中。在 Hoare 写作这篇论文的同时,面向对象方式正在成为编程的基石,并发操作并没有被给与过多的思考。Hoare 开始纠正这个现象,在接下来的 6 年里,关于 CSP 的想法被提炼成了一个叫做 "进程微积分"的正式名称来将 CSP 的想法投入到并发编程实践中。
一个进程的输出应该直接流量另一个进程的输入。一个由守护的命令仅仅是一个带有左和右倾向的语句,由 → 来分隔。左侧服务是有运行条件的,或者是守护右侧服务,如果左侧服务运行失败,或者在一个命令执行后,返回 false 或者退出,右侧服务永远不会被执行。将这些与 Hoare 的思想组合机器,为 Hoare 通信过程奠定了基础,从而实现了 channel。
Go 如何帮助你
goroutine 把我们从必须按照并行的方式思考中解放出来,作为替代,它准许我们按照更为自然的等级对问题进行建模。
channel 帮助我们把每一个 goroutine 组合在一起。
select 语句是对 channel 的一个补充,并且是多个 channel 组合的所有难点得以实现。
这里我推荐刘丹冰的 GMP 讲解
https://zhuanlan.zhihu.com/p/168610624
Go 语言的并发哲学
“使用通信来共享内存,而不是是通过共享内存来通信“ ,这句座右铭想必每一个 Gopher 都熟知。在并发代码编写中 Go 提供了并发原语(go,channel) 和 sync 包供我们选择,那么我们什么时候应该选择并发原语,什么时候选择内存访问同步呢?以下这副图给出了答案:
你是否需要转让数据所有权?
如果需要将计算结果给你共享给其他的代码块,那么你应该选择 channel。这样做有两个好处:
- 创建一个带有缓存的 channel 来实现一个低成本的在内存中的队列来解耦你的生产者和消费者
- channel 确保你的并发代码可以和其他并发代码进行组合
你是否试图在保护某个数据结构的内部状态?
当你需要保护某一个数据结构的时候这是原子操作,临界区进入最小状态,我们需要将这个行为锁起来,所以这时使用 sync 包。
你是否试图协调多个逻辑片段?
记住,channel 比内存同步原语更具有可组合性。Go 团队鼓励漫天飞的 channel ,如果满篇锁对于任何语言可能都是灾难。
这是一个性能要求很高的临界区吗?
channel 使用做内存访问同步来操作,因此它只能更慢。
追求简洁,尽量使用 channel ,并且认为 goroutine 的使用是没有成本的。