引子
老听 clojure 社区的人提起 core.async ,说它如何好用,如何简化了并发编程的模型,不由得勾起了我的好奇心,想了解一番其思想的源头:CSP 模型及受其启发的 goroutine 和 channel 。
CSP 模型
CSP 最早是由 Tony Hoare 在 1977 年提出,据说老爷子至今仍在更新这个理论模型,有兴趣的朋友可以自行查阅电子版本:www.usingcsp.com/cspbook.pdf。
严格来说,CSP 是一门形式语言(类似于 ℷ calculus),用于描述并发系统中的互动模式,也因此成为一众面向并发的编程语言的理论源头,并衍生出了 Occam/Limbo/Golang…
而具体到编程语言,如 Golang,其实只用到了 CSP 的很小一部分,即理论中的 Process/Channel(对应到语言中的 goroutine/channel):这两个并发原语之间没有从属关系, Process 可以订阅任意个 Channel,Channel 也并不关心是哪个 Process 在利用它进行通信;Process 围绕 Channel 进行读写,形成一套有序阻塞和可预测的并发模型。
goroutine
What is a goroutine? It’s an independently executing function, launched by a go statement.
It has its own call stack, which grows and shrinks as required.
It’s very cheap. It’s practical to have thousands, even hundreds of thousands of goroutines.
It’s not a thread.
There might be only one thread in a program with thousands of goroutines.
Instead, goroutines are multiplexed dynamically onto threads as needed to keep all the goroutines running.
But if you think of it as a very cheap thread, you won’t be far off.― Rob Pike
以上是 Rob Pike 在 Google I/O 2012 上给出的描述,概括下来其实就一句话:
goroutine 是开销很小的线程(不是协程,它有自己的栈),很好用,需要并发的地方就用 go 起一个 func,goroutine走起
在 Golang 中,任何代码都是运行在 goroutine里,即便没有显式的 go func()
,默认的 main 函数也是一个 goroutine。
但 goroutine 不等于操作系统的线程,它与系统线程的对应关系,牵涉到 Golang 运行时的调度器:
Golang Scheduler
调度器由三方面实体构成:
- M:物理线程,类似于 POSIX 的标准线程;
- G:goroutine,它拥有自己的栈、指令指针和维护其他调度相关的信息;
- P:代表调度上下文,可将其视为一个局部调度器,使Golang代码跑在一个线程上
三者对应关系:
上图有2个 物理线程 M,每一个 M 都拥有一个上下文(P),每一个也都有一个正在运行的goroutine(G)。
P 的数量可由 runtime.GOMAXPROCS()
进行设置,它代表了真正的并发能力,即可有多少个 goroutine 同时运行。
调度器为什么要维护多个上下文P 呢?因为当一个物理线程 M 被阻塞时,P 可以转而投奔另一个OS线程 M(即 P 带着 G 连茎拔起,去另一个 M 节点下运行)。这是 Golang调度器厉害的地方,也是高并发能力的保障。
channel
channel 就是 goroutine 之间通信(读写)的通道。
在 Golang 的并发模型里,我们并不关心是哪个 goroutine(匿名性)在用 channel,只关心 channel 的性质:
- 是只读还是只写?
- 传递的数据类型?
- 是否有缓冲区?
比如我希望在程序里并发的计算并传递一个整型值,我就会定义一个 int 型的 channel:
Govalue := make(chan int)
由于 make 这个 channel 并未提供第二个参数capacity,因此这个 channel 是不带缓冲区的,即同步阻塞的channel:
无缓冲的 channel
它有如下特点:
1. 不可以在同一个 goroutine 中既读又写,否则将会死锁,抛出如
Shellfatal error: all goroutines are asleep - deadlock!
这样的错误,以下代码片断是这种典型:
Gofunc deadlock() {
ch := make(chan int)
ch <- 2
x := <-ch
log.Println(x)
}
2. 两个goroutine中使用无缓冲的channel,则读写互为阻塞,即双方代码的执行都会阻塞在 <-ch 和 ch <- 处,只到双方读写完成在 ch 中的传递,各自继续向下执行,此处借用CSP 图例说明:
goroutine 交互的代码看起来是这样的:
Gofunc nolock() {
ch := make(chan int)
go func() {
ch <- 2
log.Println("after write")
}()
x := <-ch
log.Println("after read:", x)
}
有缓冲的 channel
在 make 时传递第二参 capacity,即为有缓冲的 channel:
Goch := make(chan int, 1)
这样的 channel 无论是否在同一 goroutine 中,均可读写而不致死锁,看看如下片断,你能猜出它会输出什么吗:
Goch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
一个粟子
网上看来的求素数的例子:使用若干个 goroutine (根据求解范围 N 而定)做素数的筛法,即
从2开始每找到一个素数就标记所有能被该素数整除的所有数。直到没有可标记的数,剩下的就都是素数。下面以找出10以内所有素数为例,借用 CSP 方式解决这个问题。
代码如下:
Gopackage main
import "fmt"
func Processor(seq <-chan int, wait chan struct{}, level int) {
go func() {
prime, ok := <-seq
if !ok {
close(wait)
return
}
fmt.Printf("[%d]: %d\n", level, prime)
out := make(chan int)
Processor(out, wait, level+1)
for num := range seq {
if num%prime != 0 {
out <- num
}
}
close(out)
}()
}
func main() {
origin, wait := make(chan int), make(chan struct{})
Processor(origin, wait, 1)
for num := 2; num < 10; num++ {
origin <- num
}
close(origin)
<-wait
}
FAQ
- Q:goroutine 什么情况下会产生 leak?
- A:channel 上只有 send 没有 receive
- Q:读一个已经关闭的 channel 只会读取到 0 值,有什么办法应对?
- A:要么在 receive 时加上第二个参数,如
v, ok :=
,要么使用v := range ch
形式接收 - Q:写一个已经关闭的 channel 会有什么结果?
- A:会 pannic
- Q:学习 Golang 有什么好的材料
- A:官网,及下边这本 Golang圣经
引用
- en.wikipedia.org/wiki/Commun…
- 36kr.com/p/5073181.h…
- arild.github.io/csp-present…
- zora.ghost.io/jian-yi-cal…
- www.jdon.com/concurrent/…
- www.jtolds.com/writing/201…
- blog.golang.org/share-memor…
- talks.golang.org/2012/concur…
- www.zhihu.com/question/20…
- blog.golang.org/pipelines
您的支持将鼓励我们继续创作!
微信支付 支付宝用 [微信] 扫描二维码打赏
用 [支付宝] 扫描二维码打赏