0. 引言
有人曾经说过,Go语言为并发而生。这么说是因为,Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。Go语言从底层原生支持并发,无须第三方库,开发人员可以很轻松地在编写程序时决定怎么使用 CPU 资源。而Go语言的并发则是基于goroutine的。
1. goroutine
1.1. goroutine简介
在Go语言中,每一个并发执行的活动叫做一个goroutine(协程)。goroutine类似于其他面向对象语言中的线程thread,它和线程并没有本质上的区别,但是要比线程小得多。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。
1.2. goroutine用法
在Go语言中,使用go关键字启动一个goroutine,例如go f()
。
当一个程序启动时,只有一个main goroutine用来执行main函数。可以在main函数中创建新的goroutine。
1.3. 例子
1 | package main |
2 | import ( |
3 | "fmt" |
4 | "time" |
5 | ) |
6 | |
7 | func main() { |
8 | go spinner(100 * time.Millisecond) // 显示计算过程中的动画 |
9 | const n = 45 |
10 | fibN := fib(n) // 计算斐波那契数列,较慢 |
11 | fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) |
12 | } |
13 | |
14 | func spinner(delay time.Duration) { |
15 | for { |
16 | for _, r := range `-\|/` { |
17 | fmt.Printf("\r%c", r) |
18 | time.Sleep(delay) |
19 | } |
20 | } |
21 | } |
22 | |
23 | func fib(x int) int { |
24 | if x < 2 { |
25 | return x |
26 | } |
27 | return fib(x-1) + fib(x-2) |
28 | } |
该例程来自于《Go程序设计语言》,用于计算斐波那契数列并在计算过程中显示动画。main函数中开启了一个goroutine来显示计算过程动画,主程序计算并打印结果。当main函数返回后,其创建的所有goroutine都会结束,程序退出。
除了main函数返回或程序退出两种情况,一个goroutine无法终止另一个goroutine。但是可以通过交流让goroutine自行停止,详见net包,此处略过。
2. Channel
2.1. Channel简介
Channel(信道)是goroutine之间沟通的桥梁。每个channel都只能传递一种特定类型,这种类型叫做channel的元素类型(element type)。例如传递int类型的channel写作chan int。
2.2. Channel用法
创建channel:通过make函数创建一个int类型的channel:ch := make(chan int)
Channel有两个主要的操作,send和receive。其中send指一个goroutine将数据通过channel发送给另一个goroutine,而接收者也需要通过channel主动接收数据才行。两者都使用<-
操作符。另外,当不再使用channel时,可以通过close(ch)
操作关闭channel。
Channel分为无缓冲和有缓冲,make函数的第二个参数便是缓冲区的大小。无缓冲channel当send或receive时,所在goroutine会阻塞,直到另外的goroutine在该channel发起receive或send;有缓冲的channel则在缓冲区满时,send才被阻塞。
2.3. 例子
下面的两个例子中分别体现了两种常用的从channel中接收数据的方法。
1 | package main |
2 | import ( |
3 | "fmt" |
4 | ) |
5 | |
6 | func main() { |
7 | chann := make(chan bool) |
8 | go func() { |
9 | fmt.Println("Goroutine") |
10 | chann <- true |
11 | chann <- false |
12 | chann <- true |
13 | close(chann) //关闭channel |
14 | }() |
15 | for v := range chann { |
16 | fmt.Println(v) |
17 | } |
18 | } |
该例在main函数中创建了信道chann,并在一个新的goroutine中向该信道传值,最后在main函数中通过range来接收并打印信道传来的每一个值。
1 | package main |
2 | import "fmt" |
3 | |
4 | func fibonacci(c, quit chan int) { |
5 | x, y := 0, 1 |
6 | for { |
7 | select { |
8 | case c <- x: |
9 | x, y = y, x+y |
10 | case <-quit: |
11 | fmt.Println("quit") |
12 | return |
13 | default: |
14 | } |
15 | } |
16 | } |
17 | |
18 | func main() { |
19 | c := make(chan int) |
20 | quit := make(chan int) |
21 | go func() { |
22 | for i := 0; i < 10; i++ { |
23 | fmt.Println(<-c) |
24 | } |
25 | quit <- 0 |
26 | }() |
27 | fibonacci(c, quit) |
28 | } |
该例中,main goroutine进行斐波那契数列计算,并将每一步的计算结果通过信道c传递给打印goroutine。打印goroutine从信道c中接收到10次数据后,给信道quit发信号,main goroutine在接收到quit信道的数据后(数据是什么无所谓)即返回,程序结束。
通过select可以实现复用,它的每一个case代表一个通信操作(在某个channel上进行send或receive)。在没有就绪的channel时,select会阻塞或者执行可选的default中的操作。在该例中,一开始打印goroutine从信道c中接收计算结果,因此case c <- x:
发送就绪;之后打印goroutine不再接收信道c的数据,而向信道quit发送数据,case c <- x:
随之阻塞,case <-quit:
就绪,执行返回操作。
3. 小结
以上便是Go语言中关于goroutine和channel的基本知识。在KubeEdge等Kubernetes相关代码中,大量用到了select和channel,对Go语言不熟悉就会看起来一头雾水,因此还是需要打好基础。