Go语言中的并发程序可以用两种手段来实现:CSP,多线程共享内存
CSP(“顺序通信进程”(communicating sequential processes))是一种现代的并发编程模型,在这种编程模型中值会在不同的运行实例(goroutine)中传递,尽管大多数情况下仍然是被限制在单一实例中
Goroutines
在Go语言中,每一个并发的执行单元叫作一个goroutine,可以简单地把goroutine类比作一个线程
当一个程序启动时,其主函数即在一个单独的goroutine中运行:main goroutine,新的goroutine会用go语句来创建(在普通函数或方法前加上go关键字),语句/函数就可以在新创建的goroutine中运行
Channels
如果goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。一个channel是一个通信机制,可以让一个goroutine通过它给另一个goroutine发送值信息
每个channel都有一个特殊的类型,即channels可发送数据的类型,一个可以发送int类型数据的channel一般写为chan int
使用内置的make函数可创建channel:
- 无缓存的channel:
ch := make(chan int) - 带缓存的channel:
ch = make(chan int, 3)
channel有发送和接受两个主要操作,都是通信行为,都使用<-运算符
一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine
- 在发送语句中,<-运算符分割channel和要发送的值
- 在接收语句中,<-运算符写在channel对象之前
- 不使用接收结果的接收操作也是合法的
close操作可用于关闭channel:close(ch),随后对基于该channel的任何发送操作都将导致panic异常,但是依然可以接受到之前已成功发送的数据;如果channel中已经没有数据将产生一个零值的数据
不带缓存的channels
一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作,当发送的值通过Channels成功传输之后,两个goroutine可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者goroutine也将阻塞,直到有另一个goroutine在相同的Channels上执行发送操作
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作,因此,无缓存Channels有时候也被称为同步Channels
当两个goroutine并发访问了相同的变量时,有必要保证某些事件的执行顺序,以避免出现某些并发问题
串联的Channels
一个Channel的输出作为下一个Channel的输入,这种串联的Channels就是管道(pipeline)
两个Channels将三个goroutine串联,如下图:
// 将生成0、1、4、9、……形式的无穷数列
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; ; x++ {
naturals <- x
}
}()
// Squarer
go func() {
for {
x := <-naturals
squares <- x * x
}
}()
// Printer (in main goroutine)
for {
fmt.Println(<-squares)
}
}
没有办法直接测试一个channel是否被关闭,但是接收操作有一个变体形式:它多接收一个结果,多接收的第二个结果是一个布尔值ok,ture表示成功从channels接收到值,false表示channels已经被关闭并且里面没有值可接收
// Squarer
go func() {
for {
x, ok := <-naturals
if !ok {
break // channel was closed and drained
}
squares <- x * x
}
close(squares)
}()
单方向的Channels
当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收(单方向)
箭头<-和关键字 chan 的相对位置表明了channel的方向:
- 类型
chan<- int表示一个只发送 int 的 channel,只能发送不能接收 - 类型
<-chan int表示一个只接收 int 的 channel,只能接收不能发送
关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将编译错误
带缓存的Channels
带缓存的Channel内部持有一个元素队列,队列的最大容量是在调用make函数创建channel时通过第二个参数指定的
ch = make(chan string, 3)
向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素
如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个goroutine执行接收操作而释放了新的队列空间。相反,如果channel是空的,接收操作将阻塞直到有另一个goroutine执行发送操作而向队列插入元素。
内置的cap函数获取channel内部缓存的容量:cap(ch)
内置的len函数获取channel内部缓存队列中有效元素的个数:len(ch)
如果多个goroutines并发地向同一个无缓存channel发送数据,可能会出现goroutines泄漏,bug。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此确保每个不再需要的goroutine能正常退出是重要的
基于select的多路复用
select语句,和switch语句稍微有点相似,也会有几个case和最后的default选择分支。每一个case代表一个通信操作(在某个channel上进行发送或者接收),并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身(不把接收到的值赋值给变量什么的),就像下面的第一个case,或者包含在一个简短的变量声明中,像第二个case里一样;第二种形式能够引用接收到的值
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default: // 当其它的操作都不能够马上被处理时程序需要执行哪些逻辑
// ...
}
select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;这时候其它通信是不会执行的。一个没有任何case的select语句写作select{},会永远地等待下去
如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机会
并发的退出
并发的退出是指在 Go 语言中,如何让一个或多个 goroutine 在满足一定条件后停止运行的问题
Go 语言并没有提供终止 goroutine 的接口,即不能从外部去停止一个 goroutine,只能由 goroutine 内部退出
有多种方法可以实现并发的退出,例如:
- 使用 for-range 结构:这种方法适用于从单一通道上获取数据并执行任务的场景。当通道关闭时,for-range 循环会自动结束,从而退出 goroutine
- 使用 for-select 结构:这种方法适用于监听多个通道的场景。可以定义一个特定的退出通道,用于接收退出信号。当收到退出信号时,可以使用 return 语句或者将通道赋值为 nil 来退出 goroutine
- 使用 context 包:这是官方提供的一个用于控制多个 goroutine 协作的包。可以使用 context.WithCancel 函数来创建一个可取消的子 context,并传递给需要退出的 goroutine。当调用 cancel 函数时,所有监听 context.Done 通道的 goroutine 都会收到关闭信号,并退出