关键词
channel、阻塞、panic、select
导语
Channel是Golang语言的特色,用于多个Goroutine之间的通信。本文首先探讨对其发送、接收操作的特点,然后讨论什么时候channel会阻塞、什么时候channel会panic,最后讨论专门为channel而生的select语句
channel通道发送与接收操作的特点
- 发送操作与接收操作都是互斥的
解释:多个goroutine中同时向同一个channel发送或者接收时,只能一个goroutine对channel进行发送或接收,发送之间互斥,接收之间也互斥。
- 发送操作在完全完成之前会被阻塞,接收操作也是如此。
- 发送操作和接收操作中对元素值的处理都是不可分割的。
解释:
发送操作,不是直接将值将入到
channel,而是第一步、进行复制,第二步、将副本加入到channel中,这个过程不可分割且会阻塞。 接收操作,不是直接将值直接赋值给变量,而是第一步、进行复制,第二步、将副本赋值给变量,第三部、删除channel中的相应元素。这个过程不可分割且会阻塞。
对通道的发送与接收操作,什么时候会阻塞?
- 通道为
nil时,发送与接收操作全都会阻塞
解释:通道channel为引用类型,只声明不用make初始化,此时channel就是一个nil
package main
import (
"fmt"
"time"
)
func main() {
var ch chan int
go func() {
ch <- 1
fmt.Println("goroutine finish")
}()
time.Sleep(10*time.Second)
}
// main goroutine 会等待10s,不会打印goroutine finish,说明一直阻塞
- 缓冲通道,已满,进行发送操作,发送操作阻塞
package main
import (
"fmt"
"time"
)
func main() {
var ch = make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
fmt.Println("channel is fill")
go func() {
ch <- 4
fmt.Println("fill channel can receive a number?")
}()
time.Sleep(5 * time.Second)
}
- 缓冲通道,已空,进行接收操作,接收操作阻塞
- 非缓冲通道,只有当发送和接收都准备好(其实就是同步),才不会阻塞。
package main
func main() {
// 示例1。
ch1 := make(chan int, 1)
ch1 <- 1
//ch1 <- 2 // 通道已满,因此这里会造成阻塞。
// 示例2。
ch2 := make(chan int, 1)
//elem, ok := <-ch2 // 通道已空,因此这里会造成阻塞。
//_, _ = elem, ok
ch2 <- 1
// 示例3。
var ch3 chan int
//ch3 <- 1 // 通道的值为nil,因此这里会造成永久的阻塞!
//<-ch3 // 通道的值为nil,因此这里会造成永久的阻塞!
_ = ch3
}
通道的接收与发送什么时候会引发panic?
一句话总结:跟通道channel关闭有关
- 通道已经关闭,但是依然向其发送元素
package main
func main() {
var ch = make(chan int, 2)
close(ch)
ch <- 1
}
- 再次关闭已经关闭的通道
package main
func main() {
var ch = make(chan int, 2)
close(ch)
//ch <- 1
close(ch)
}
一个正常的收发channel示例
package main
import "fmt"
func main() {
var ch = make(chan int, 2)
go func() {
for i := 0; i < 10; i++ {
fmt.Println("sender channel a val: ", i)
ch <- i
}
fmt.Println("sender finished, will close the channel")
close(ch)
}()
for {
elem, ok := <- ch
if !ok {
fmt.Println("channel has already closed.")
break
} else {
fmt.Println("receive a val from channel, ", elem)
}
}
fmt.Println("End.")
}
select
基础概念
select语句是专门为了通道channel而设计的,所以select的每个case表达式中,都只能包含操作通道的表达式,比如接收表达式和发送表达式
select本质上就是对通道channel的I/O操作的监听器。
// 给定几个通道,哪个通道不为空,便执行相应语句
// 准备好几个通道。
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的case分支必须是关于通道的发送表达式或者接收表达式。- 如果
select有默认的default分支,那么无论case条件中对通道的操作是否阻塞,select语句是不会阻塞的。 select语句中只能有一个default分支,且分支的位置不重要。都是evaluated各个case分支之后且不满足的时候才会执行default分支的语句。- 如果
select没有默认的default分支,而且case条件中对通道的操作都阻塞,select语句会阻塞的。如果发生阻塞,就认为该case表达式不满足执行条件,进行下一个case分支的判断。 - 一个
select语句只能对其中的每个case表达式各求值一次,如果想要迭代求值,需要for语句的帮助。但是请注意,如果此时select中的case语句当中有break执行,那么并不会跳出外层的for语句,而是会继续循环。 - 如果
select语句发现同时有多个case分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个执行。注意,即使select语句是在被唤醒时发现的这种的情况,也会这么做。
func example2() {
intChan := make(chan int, 1)
// 一秒后关闭通道。
time.AfterFunc(time.Second, func() {
close(intChan)
})
select {
case _, ok := <-intChan:
if !ok {
fmt.Println("The candidate case is closed.")
break
}
fmt.Println("The candidate case is selected.")
}
}
// result
The candidate case is closed.
程序分析,代码运行到select的唯一case分支时,因为此时intChan为空,无法进行接收,所以阻塞,1s之后,通道关闭,二元接收值中的ok为false,所以执行答应The candidate case is closed.
再次看一个示例,到底哪个case被执行,或者default被执行
package main
import "fmt"
var channels = [3]chan int{
nil,
make(chan int),
nil,
}
var numbers = []int{0, 1, 2}
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]
}
// result
No candidate case is selected!
为何?
因为三个case分支全都阻塞
第一、第三:试图向nil通道发送数据,阻塞之
第二个试图向无缓冲通道发送,因为没有相应的接收操作,阻塞之
所以只能执行default分支的语句
如果想要第二个case分支运行,只需要做如下修改
package main
import "fmt"
var channels = [3]chan int{
nil,
make(chan int),
nil,
}
var numbers = []int{0, 1, 2}
func main() {
go func() {
for {
<- getChan(1)
}
}()
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!")
}
fmt.Println("finish")
}
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]
}
起一个goroutine,接收无缓冲通道的元素
参考资料
1、极客时间《Go语言核心36讲》