Golang CSP理解

274 阅读4分钟

Golang CSP理解

CSP介绍

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通信,而要通过通信来实现内存共享。

这就是 Go 的并发哲学,它依赖 CSP 模型,基于 channel 实现。

CSP 经常被认为是 Go 在并发编程上成功的关键因素。CSP 全称是 “Communicating Sequential Processes”,这也是 Tony Hoare 在 1978 年发表在 ACM 的一篇论文。论文里指出一门编程语言应该重视 input 和 output 的原语,尤其是并发编程的代码。

在那篇文章发表的时代,人们正在研究模块化编程的思想,该不该用 goto 语句在当时是最激烈的议题。彼时,面向对象编程的思想正在崛起,几乎没什么人关心并发编程。

在文章中,CSP 也是一门自定义的编程语言,作者定义了输入输出语句,用于 processes 间的通信(communication)。processes 被认为是需要输入驱动,并且产生输出,供其他 processes 消费,processes 可以是进程、线程、甚至是代码块。输入命令是:!,用来向 processes 写入;输出是:?,用来从 processes 读出。这篇文章要讲的 channel 正是借鉴了这一设计。

Hoare 还提出了一个 -> 命令,如果 -> 左边的语句返回 false,那它右边的语句就不会执行。

通过这些输入输出命令,Hoare 证明了如果一门编程语言中把 processes 间的通信看得第一等重要,那么并发编程的问题就会变得简单。

Go 是第一个将 CSP 的这些思想引入,并且发扬光大的语言。仅管内存同步访问控制(原文是 memory access synchronization)在某些情况下大有用处,Go 里也有相应的 sync 包支持,但是这在大型程序很容易出错。

Go 一开始就把 CSP 的思想融入到语言的核心里,所以并发编程成为 Go 的一个独特的优势,而且很容易理解。

大多数的编程语言的并发编程模型是基于线程和内存同步访问控制,Go 的并发编程的模型则用 goroutine 和 channel 来替代。Goroutine 和线程类似,channel 和 mutex (用于内存同步访问控制)类似。

Goroutine 解放了程序员,让我们更能贴近业务去思考问题。而不用考虑各种像线程库、线程开销、线程调度等等这些繁琐的底层问题,goroutine 天生替你解决好了。

Channel 则天生就可以和其他 channel 组合。我们可以把收集各种子系统结果的 channel 输入到同一个 channel。Channel 还可以和 select, cancel, timeout 结合起来。而 mutex 就没有这些功能。

Go 的并发原则非常优秀,目标就是简单:尽量使用 channel;把 goroutine 当作免费的资源,随便用。

代码讲解

共享内存是线程之间进行通信的传统方式。Go具有内置的同步功能,允许单个goroutine拥有共享数据。这意味着其他 goroutine必须向拥有共享数据的单个goroutine发送消息,这样的单个goroutine称为监视器goroutine。在Go术语中,这是通过通信来实现内存共享。

通过代码shared_mem.go加以说明:

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)
// 用两个chan实现共享值value的读写
// 从read chan读共享值
var readValue = make(chan int)

// 写共享值到write chan
var writeValue = make(chan int)

// 往write chan中写
func SetValue(newValue int) {
	writeValue <- newValue
}

// 从read chan读取共享值
func ReadValue() int {
	return <-readValue
}

// 监视器goroutine 通过select协调共享值的读写
func monitor() {
	var value int // 保存当前值 用于读取共享变量
	for {
		select {
		case newValue := <-writeValue: // 当前值给value保存
			value = newValue
			fmt.Printf("%d ", value)
		case readValue <- value: // 如果这个时候有goroutine读取的话 就是最新当前值
		}
	}
}

func main() {
	rand.Seed(time.Now().Unix())
	go monitor()
	var waitGroup sync.WaitGroup

	for r := 0; r < 20; r++ {
		waitGroup.Add(1)
		go func() {
			defer waitGroup.Done()
			SetValue(rand.Intn(100))
		}()
	}

	waitGroup.Wait()
	fmt.Printf("\nLast value: %d\n", ReadValue())
}

结果如下:

$ go run shared_mem.go
33 52 60 72 56 76 66 36 75 76 29 26 90 30 64 46 64 46 51 70 
Last value: 70
$ go run shared_mem.go
8 62 36 87 11 98 19 82 3 68 41 4 36 7 63 77 89 74 56 3 
Last value: 3

最后一次结果截图如下: