Go 的并发机制
4.3 channel
“应该以通信作为手段来共享内存” 的最直接和最重要的体现: channel。它提供了一种机制,既可以同步两个并发执行的函数,也可以让这两个函数通过相互传递特定类型的值来通信。
4.3.1 channel 的基本概念
channel 既指通道类型,也指代可以传递某种类型的值的通道。
1. 类型表示法
- 通道类型也属于引用类型
- 双向通道:var intChan chan int
- 接收通道:chan<- T
- 发送通道:<-chan T
2. 值表示法
- 因为通道类型是一个引用类型,所以一个通道类型的变量在初始化之前,其值一定是 nil,亦是此类型的零值
- 通道类型的变量是用来传递值的,而不是存储值的。所以,通道类型并没有对应的值表示法。
- 它的值具有即时性,是无法用字面量来准确表达的
3. 操作的特性
- 通道是在多个 goroutine 之间传递数据和同步的重要手段,而对通道的操作本身也是同步的。
- 同一时刻,仅有一个 goroutine 能想一个通道发送元素值,同时也仅有一个 goroutine 能从它那里接收元素值
- 各个元素值都是严格按照发送到此的先后顺序排列的,最早被发送至通道的元素值会最先被接收
- 通道中的元素值都具有原子性,是不可被分割的
4. 初始化通道
- 引用类型的值都需要使用内建函数 make 来初始化,非缓冲通道:make(chan int) ,缓冲通道:make(chan int, 10)
- 当通道满了,发送方的 goroutine 会被阻塞,知道有接收方接收这个元素
5. 接收元素值
- elem := <- strChan:会使得当前 goroutine 被阻塞在这里,因为现在通道里面没有任何值。当前 goroutine 会进入 Gwaiting 状态,直到有数据才会被唤醒。
- 试图从一个未被初始化的通道值接收元素值,会造成当前 goroutine 的永久阻塞
6. Happens before
为了能够从通道接收元素值,我们先向它发送元素值。对于缓冲通道来说:
- 发送操作会使通道复制被发送的元素。如果通道的缓冲空间已满而无法立即复制,则阻塞进行发送操作的 goroutine。当通道已空且有接收方在等待元素值时,它会是最早等待的那个接收方持有的内存地址,否则会是通道持有的缓冲中的内存地址。
- 接收操作会使通道给出一个已发给它的元素值的副本,如果因通道的缓冲空间已空而无法立即给出,则阻塞进行接收操作的 goroutine。
- 对于同一个元素值来说,把它发送给某个通道的操作,一定会从该通道接收它的操作完成之前完成。换而言之,在通道完全复制一个元素值之前,任何 goroutine 都不能从它那里接收到这个元素值的副本。
7. 发送元素值
对接收操作符 <- 两边的表达式的求值会先于发送操作执行。以下举个两个 goroutine 通过 channel 交互的例子:
package main
import (
"fmt"
"time"
)
var strChan = make(chan string, 3)
func main() {
syncChan1 := make(chan struct{}, 1)
syncChan2 := make(chan struct{}, 2)
go func() { // 用于演示接收操作。
<-syncChan1
fmt.Println("Received a sync signal and wait a second... [receiver]")
time.Sleep(time.Second)
for {
if elem, ok := <-strChan; ok {
fmt.Println("Received:", elem, "[receiver]")
} else {
break
}
}
fmt.Println("Stopped. [receiver]")
syncChan2 <- struct{}{}
}()
go func() { // 用于演示发送操作。
for _, elem := range []string{"a", "b", "c", "d"} {
strChan <- elem
fmt.Println("Sent:", elem, "[sender]")
if elem == "c" {
syncChan1 <- struct{}{}
fmt.Println("Sent a sync signal. [sender]")
}
}
fmt.Println("Wait 2 seconds... [sender]")
time.Sleep(time.Second * 2)
close(strChan)
syncChan2 <- struct{}{}
}()
<-syncChan2
<-syncChan2
}
运行结果如下:
Sent: a [sender]
Sent: b [sender]
Sent: c [sender]
Sent a sync signal. [sender]
Received a sync signal and wait a second... [receiver]
Sent: d [sender]
Wait 2 seconds... [sender]
Received: a [receiver]
Received: b [receiver]
Received: c [receiver]
Received: d [receiver]
Stopped. [receiver]
- syncChan1和syncChan2 的元素类型都是 struct{},一般用于传递“信号”的通道都以这个作为元素类型。
- 当向一个值为 nil 的通道类型的变量发送元素值时,当前 goroutine 也会被永久地阻塞
- 试图向一个已关闭的通道发送元素值,会立即引发一个运行时恐慌,即使发送操作正在因通道已满而被阻塞
- Go 运行时系统每次只会唤醒一个 goroutine
- 发送方向通道发送的值会被复制,接收方接收的总是该值的副本,而不是该值本身。经由通道传递的值至少会被复制一次,至多会被复制两次。
- 当接收方从通道接收到一个值类型的值时,对该值的修改不会影响到发送方持有的那个源值。但是对于引用类型来说,会同时影响收发双方持有的值。
- 但是如果包含了切片,一样会影响双方持有的值(指针)
8. 关闭通道
- 无论如何都不应该在接收方关闭通道
- 一个通道仅可以关闭一次,不能重复关闭
- 不能关闭 nil 通道
9. 长度和容量
- len 长度:通道当前的元素值数量
- cap 容量:通道的容量初始化的时候就确定了
4.3.2 单向 channel
一般声明一个双向通道,然后发送方和接收方分别对其做发送、接收操作(控制方向)。
4.3.3 for 语句与 channel
- 从一个还未被初始化的通道中接收元素值会导致当前 goroutine 的永久阻塞,使用 for 语句也不例外
- for 语句会不断尝试从通道中接收元素值,直到该通道关闭
4.3.4 select 语句
- case 关键字右边的发送语句或接收语句中的通道表达式和元素表达式都会先求值(求值的顺序是从左至右,从上到下的),无论是否会被选择到对应分支
- 当有一个 case 被选中时,运行时系统就会执行该 case 及其包含的语句,而其他 case 会被忽略。
- 如果同时多个case 被选中,那么运行时系统会通过一个伪随机的算法选中一个 case。
- 如果没有 default case,并且其他所有 case 都不满足,当前 goroutine 会被一直阻塞于此。
- 最好单独把 select 语句放到一个 goroutine 中执行,这样即使它阻塞了,也不会造成死锁
4.3.5 非缓冲的 channel
非缓冲通道只能同步传递元素值
1. happens before
- 向此类通道发送元素值的操作会被阻塞,直到至少有一个针对该通道的接收操作进行为止。该操作会先得到元素值的副本,然后在唤醒发送方所在的 goroutine 之后返回。也即,接收操作会在对应的发送操作完成之前完成。
- 从此类通道接收元素值的操作会被阻塞,直到至少一个针对该通道的发送操作进行为止。该发送操作会直接把元素值复制给接收方,然后在唤醒接收方所在的 goroutine 之后返回。也即,发送操作会在对应的接收操作完成之前完成。
2. 同步的特性
- 由于非缓冲通道会以同步的方式传递元素值,在其上收发元素值的速度总是与慢的那一方持平