通道是 Go 并发编程中重要的一员
基础知识
- 通道是 Go 自带的,唯一一个可以满足并发安全性的类型。
- 声明并初始化一个通道,需要使用 make 函数。
- 通道相当于一个队列,它是先进先出的。对于通道的输入和输出,需要使用 <- 操作符。
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",
elem1)
}
- 在上面的代码中,声明并初始化了一个可以存储 3 个 int 类型的通道,并向通道发送了 2、1、3 三个元素。
- 你可以使用短变量声明的方式直接为一个变量赋值。如 elem1 := <-ch1
通道的基本特性
- 对于同一个通道,发送操作之间互斥,接收操作之间也是互斥的。
- 发送操作和接收操作中对元素的值的处理都是不可分割的。
- 不论是发送操作还是接收操作,它们在完全完成之间都会被阻塞。
通道之所以有这三个特性,是为了实现操作的互斥和元素值的完整。下面是这些特性中的一些细节:
- 元素值从外界进入通道时会被复制,也就是说,通道中的元素是赋值元素的副本。
- “不可分割”的意思是,它们处理元素值是一气呵成的,绝不会被打断。
- 发送操作包括:复制元素值 和 放置副本到通道 这两个步骤。
- 接收操作包括:复制通道内的元素、放置副本到接收方 和 删除原值 三个步骤。
通道何时会被阻塞
- 通道满时,对它的所有发送操作都被阻塞,知道通道中有元素被接收。
- 通道空的时候,接收操作会被阻塞。注意,阻塞唤醒的顺序也是由协程到来的顺序决定的。(也就是说,所有进入到通道内的元素依然保持 FIFO 的特性)
- 对于非缓冲的通道(不设置缓冲值),则发送和接收都会被阻塞,直到对方到来。
可能会引发错误的操作
- 通道是引用类型,所以它的零值是 nil,这种情况下任何发生和接收的操作都会造成永久阻塞,一定要记得使用 make 函数进行初始化。
- 对已经 close 的通道进行发送操作,会引发 panic。
- 重复 close 一个已经关闭的通道,会引发 panic。
- 你可以接收两个值,第二个值如果为 false,代表通道已经关闭,且通道中没有元素了:
elem, ok := <-ch2
- 因为上面的原因,建议不要让接收方关闭通道,而是让发送方关闭。
通道的高级操作
单向通道
- 你可以在 make 的时候指定通道的类型:
var uselessChan = make(chan<- int, 1)
- 对于上面的通道,我们只能向通道中发送数据,而不能接收。
- 单向通道的用处,是约束其他代码的行为,你可以看一下下面的代码:
func SendInt(ch chan<- int) {
ch <- rand.Intn(1000)
}
- 在这个函数中,函数能接受一个 chan<- int 类型的参数,这就起到了约束的作用。同样的道理,你可以使用接口来约束相关代码实现:
type Notifier interface {
SendInt(ch chan<- int)
}
- 注意,你可以将双向通道作为参数传入,Go 会自动将双向通道转换为函数所需要的单向通道:
intChan1 := make(chan int, 3)
SendInt(intChan1)
- 返回值也是如此:
func getIntChan() <-chan int {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
return ch
}
for ... range
- for 和 range 可以配合通道使用:
intChan2 := getIntChan()
for elem := range intChan2 {
fmt.Printf("The element in intChan2: %v\n", elem)
}
- for 会不断从通道中取出元素,直到通道已经关闭 且 通道已经没有元素。
- 如果通道中没有元素且没有被关闭,for 会阻塞。
- 如果通道为 nil,则会永远阻塞。
select
- select 只能与通道联合使用,下面是一个例子:
// 准备好几个通道。
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
fmt.Println("No candidate case is selected!")
}
在使用 select 的时候,你需要注意以下几个事情
- 如果你设置了 default,可以避免阻塞。
- 如果没有 default,且所有 case 表达式都没有满足的条件,select 就会被阻塞。知道至少有一个 case 满足条件。
- 你最好添加对通道是否关闭的判断,并在某个通道已经关闭的时候屏蔽掉它。
- select 只能对每个 case 求值一次,如果你想持续地操作其中的通道,需要使用 for。
- 但是,break 只能跳出 select,而不能直接跳出 for,你需要警惕 for 无限运行。
select 语句分支选择规则
- 在 select 进行选择之前,会对所有的 case 进行扫描,扫描完成后,才会开始选择分支。
- select 扫描的方式:从上到下,从左到右。
- 如果一个 case 表达式(这个表达式甚至可以是由函数返回)处于阻塞,在后续的选择分支时就会屏蔽这个 case。
- 如果一个 select 中有多个 case 符合条件,go 会使用一种伪随机的方式选择一个 case 执行。
- 一个 select 只能有一个默认分支(default)
- select 的执行和对 case 表达式的求值和选择都是独立的。但是,它们的执行是否并发安全,需要看一下具体的代码。
- 下面是一个代码实例:
package main
import "fmt"
var channels = [3]chan int{
nil,
make(chan int),
nil,
}
var numbers = []int{1, 2, 3}
func main() {
select {
case getChan(0) <- getNumber(0):
fmt.Println("The first candidate case is selected.")
case getChan(1) <- getNumber(1):
fmt.Println("The second candidate case is selected.")
case getChan(2) <- getNumber(2):
fmt.Println("The third candidate case is selected")
default:
fmt.Println("No candidate case is selected!")
}
}
func getNumber(i int) int {
fmt.Printf("numbers[%d]\n", i)
return numbers[i]
}
func getChan(i int) chan int {
fmt.Printf("channels[%d]\n", i)
return channels[i]
}
// 执行结果:
channels[0]
numbers[0]
channels[1]
numbers[1]
channels[2]
numbers[2]
No candidate case is selected!
解释:1. 在这个 select 中,会从上到下,从左到右依次执行 case 中的表达式,在这个过程中,会触发在函数中的 print 操作。
2.在扫描完成后,发现所有的 case 都是阻塞的。(nil 总是阻塞,没有缓冲区的 chan 在没有数据的时候也是被阻塞的。)
3.最终,select 选择了 default 分支进行执行。
网友评论