本文最初发表在我的个人博客,查看原文,获得更好的阅读体验
并发是每个编程语言绕不开的一个话题,Go在并发编程方面提供了许多特性,帮助简化并发模型,如轻量级的线程goroutine
,信道等,同样也提供了如sync.Mutex
等的锁机制。
为实现对共享变量的正确访问,Go语言提供了一种特殊的控制方式,即将共享的值通过信道传递。信道是一种带有方向的管道,数据可以在其中流转。在任意一个的时间点,只有一个goroutine
能够访问该值,既无需加锁,也无需同步。数据竞争从设计上就被杜绝了。这种思想,被总结为一句话:
不要通过共享内存来通信,而应通过通信来共享内存。
Go的并发处理方式源于Hoare的通信顺序处理(Communicating Sequential Processes, CSP)
一 并发处理
1.1 快速开始
在Go中,通过使用关键字go
,可以快速创建一个goroutine
,例如:
package main
import (
"fmt"
"time"
)
func main() {
go printWord("A") // 开启一个新的goroutine
printWord("C")
}
func printWord(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(500 * time.Millisecond)
}
}
上面一个例子中,在多核CPU系统中,会交替打印出字母"A"、“C”,在单核CPU中则稍有不同。
1.2 Goroutine
goroutine
是一种轻量级的线程。它具有简单的模型:它与其它goroutine
并发运行在同一地址空间,因此,访问共享的内存时必须进行同步(或者使用信道)。它的所有消耗几乎就只有栈空间的分配,而且栈最开始是非常小的,所以它们很廉价,仅在需要时才会随着堆空间的分配(和释放)而变化。
goroutine
在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待I/O
,那么其它的线程会继续运行。goroutine
的设计隐藏了线程创建和管理的诸多复杂性。
直接在函数或方法前添加go
关键字即可在新的goroutine
中调用它。当调用完成后,该goroutine
也会安静地退出。(效果有点像Unix Shell中的&
符号,它可以让命令在后台运行。)
有些地方将Goroutine翻译为
Go程
,但这个词感觉并不怎么合适(也不好听),所以本文索性就将其称之为goroutine
。
前面的示例中我们已经看到过:
func main() {
go printWord("A") // 开启一个新的goroutine
printWord("C")
}
func printWord(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(500 * time.Millisecond)
}
}
有时,为了简化程序,可以直接将函数定义为一个函数字面量(即匿名函数):
package main
import (
"fmt"
"time"
)
func main() {
s := "hello"
go func() {
// 函数字面量,开启一个新的goroutine
for i := 0; i < 5; i++ {
fmt.Println(s+":", i)
time.Sleep(500 * time.Millisecond)
}
}()
// 等待主程序运行结束
time.Sleep(3000 * time.Millisecond)
}
这些示例都是一些简单的函数,要想体现出Go的精妙,还需要配合另一种数据类型,即信道。
二 信道
信道(channel)是一种重要的数据类型,既可以用作信号量,也可以用于数据传递,其结果值充当了对底层数据结构的引用。信道具有方向,可选的信道操作符<-
指定了通道的方向,发送或接收。如果没有给出方向,则它是双向的,信道可以通过分配或显式转换来限制只发送或接收。信道的零值是nil
,尝试往未初始化的信道或已关闭的信道发送或接收值都会导致运行时恐慌。
chan<- float64 // 单向信道,只能用于发送float64的值
<-chan int // 单向信道,只能用于接收int值
<-
运算符与最左边的chan关联:
chan<- chan int // 同 chan<- (chan int)
chan<- <-chan int // 同 chan<- (<-chan int)
<-chan <-chan int // 同 <-chan (<-chan int)
chan (<-chan int)
“箭头”就是数据流的方向。
2.1 无缓冲信道
和映射一样,在使用前需要先使用make
初始化:
// 创建一个可用于发送和接收字符串类型的信道
c := make(chan string)
重新看一下本文开头的例子,如果我们只保留一个goroutine
中的打印,会怎么样:
package main
import (
"fmt"
"time"
)
func main() {
go printWord("A") // 开启一个新的goroutine
}
func printWord(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
time.Sleep(500 * time.Millisecond)
}
}
结果发现,这次什么也没有输出。
为什么?一方面,新开启的goroutine
不会阻塞当前的main
函数,另一方面,一旦main函数执行完毕,整个程序也就结束,所以上面那个goroutine
还没来得及打印,主程序就已经结束了。除了用time.Sleep
函数让main函数等待一段时间,我们还可以借助信道来更优雅的实现:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan bool)
go printWord("A", c) // 开启一个新的goroutine
<-c // 等待打印完毕,丢弃传递过来的值
}
func printWord(s string, c chan bool) {
for i := 0; i < 5; i++ {