给个小建议:如果是初学者,建议把基础知识朗读一遍,有个大概印象,后面思考多了,就会“由量变达到质变”,从而有所顿悟。
目录
- 一、基础知识
- 二、例子1
- 1、管道ch的缓冲区为10,select中有case读取管道的数据
- 代码示例
- 输出结果
- 2、管道ch的缓冲区为10,~~select中有case读取管道的数据~~
- 代码示例
- 输出结果
- 3、~~管道ch的缓冲区为10~~ ,select中有case读取管道的数据
- 代码示例
- 输出结果
- 4、~~管道ch的缓冲区为10,select中有case读取管道的数据~~
- 代码示例
- 输出结果
- 5、补充知识:channel底层的等待队列
- (1)协程阻塞,加入等待队列
- 举例:下面展示了一个没有缓冲区的管道,有几个协程阻塞等待读数据。
- (2)协程被唤醒:
- (3)同一个协程里面,管道无缓冲区,会死锁
- 代码示例
- 输出结果
- 底层示意图
- (4)同一个协程里面,管道有缓冲区,不会死锁
- 代码示例
- 输出结果
- 底层示意图
- 注意:
- 6、补充知识:select
- 7、总结
- 三、例子2
- 代码示例
- 输出结果
- select 语句的执行顺序
- 四、参考资料
一、基础知识
- 在Go语言中,管道是协程间通信的方式
- 管道是nil,则读写都会永久阻塞
- 管道关闭(通过close()函数关闭),则读 ✅;写 ❌,会触发panic
- 数据读写
- 创建管道的两种方式:
// 声明管道,值为nil var ch chan int // make创建无缓冲管道和带缓冲管道 ch1 := make(chan string) ch2 := make(chan string, 10)
- 管道是双向可用的,在函数间传递时,可以通过操作符(<-)来控制管道的可读和可写。
func ChanParamRW(ch chan int) { // 管道可读写 } func ChanParamR(ch <- chan int) { // 只能从管道读取数据 } func ChanParaW(ch chan<- int) { // 只能向管道写入数据 }
二、例子1
1、管道ch的缓冲区为10,select中有case读取管道的数据
代码示例
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 20; i++ {
select {
case ch <- 1:
fmt.Println("输入1")
case ch <- 2:
fmt.Println("输入2")
case <-ch:
fmt.Println("输出")
default:
fmt.Println("default")
}
}
}()
time.Sleep(time.Millisecond)
}
输出结果
输入2
输入2
输入2
输入1
输入2
输入2
输出
输入2
输入2
输入2
输入1
输入1
输出
输入1
输出
输入2
输出
输出
输出
输入1
2、管道ch的缓冲区为10,select中有case读取管道的数据
代码示例
func main() {
ch := make(chan int, 10)
go func() {
for i := 0; i < 20; i++ {
select {
case ch <- 1:
fmt.Println("输入1")
case ch <- 2:
fmt.Println("输入2")
default:
fmt.Println("default")
}
}
}()
time.Sleep(time.Millisecond)
}
输出结果
输入2
输入1
输入1
输入1
输入2
输入2
输入2
输入2
输入1
输入1
default
default
default
default
default
default
default
default
default
default
3、管道ch的缓冲区为10 ,select中有case读取管道的数据
代码示例
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 20; i++ {
select {
case ch <- 1:
fmt.Println("输入1")
case ch <- 2:
fmt.Println("输入2")
case <-ch:
fmt.Println("输出")
default:
fmt.Println("default")
}
}
}()
time.Sleep(time.Millisecond)
}
输出结果
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
4、管道ch的缓冲区为10,select中有case读取管道的数据
代码示例
`func main() {
ch := make(chan int)
go func() {
for i := 0; i < 20; i++ {
select {
case ch <- 1:
fmt.Println("输入1")
case ch <- 2:
fmt.Println("输入2")
default:
fmt.Println("default")
}
}
}()
time.Sleep(time.Millisecond)
}
输出结果
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
default
5、补充知识:channel底层的等待队列
在runtime包中hchan定义了管道的数据结构,里面有recvq和sendq这两个等待队列。
type hchan struct {
```
recvq waitq // 等待读消息的协程队列
sendq waitq // 等待写消息的协程队列
```
}
(1)协程阻塞,加入等待队列
- 从管道「读取数据」时,如果「管道缓冲区为空」或「没有缓冲区」,则当前协程会被阻塞,并被加入recvq队列。
- 向管道「写入数据」时,如果「管道缓冲区已满」或「没有缓冲区」,则当前协程会被阻塞,并被加入sendq队列。
举例:下面展示了一个没有缓冲区的管道,有几个协程阻塞等待读数据。
(2)协程被唤醒:
- 因「读阻塞的协程」会被「向管道写入数据的协程」唤醒;
- 因「写阻塞的协程」会被「从管道读取数据的协程」唤醒。
处于等待队列中的协程会在其他协程操作管道时被唤醒。
问:为什么说是其他协程呢?
答: 加入到等待队列中的协程应该是阻塞的才对,不管是读操作阻塞还是写操作阻塞,在同一个协程程序中后面的读写操作都是无法执行的,这个时候就只能由其他协程来写或读帮助唤醒这个协程,如果没有其他协程帮忙唤醒,就会出现死锁。
如果没有缓冲区的情况下,在同一个协程内进行写和读,因为没有其他协程去读这个管道,就会一直阻塞写这一步,从而出现死锁;
如果有缓冲区并且缓冲区未满的情况下,因为有缓冲区帮忙缓冲数据,所以在同一个协程内进行读和写是没有问题的。
(3)同一个协程里面,管道无缓冲区,会死锁
代码示例
func main() {
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
time.Sleep(time.Millisecond)
}
输出结果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/Users/dns/GolandProjects/code/select/demo01.go:10 +0x37
exit status 2
底层示意图
(4)同一个协程里面,管道有缓冲区,不会死锁
代码示例
func main() {
ch := make(chan int, 10)
ch <- 1
fmt.Println(<-ch)
time.Sleep(time.Millisecond)
}
输出结果
1
底层示意图
注意:
① 一般情况下,recvq 和 sendq 至少有一个为空(因为对于同一个管道而言,如果有读协程的话,那么等待队列中就不会有写协程,如果是有读有写,协程就不会阻塞,就不会被添加到等待队列中)。
② 特殊情况:同一个协程使用select语句向管道一边写入数据,一边读取数据,此时协程就会分别位于两个等待队列中。
具体来说就是:
- 在一个协程中用select去监听「该协程向一个管道写入数据」,并且同时去监听「该协程向同一个管道读取数据」;
- 该协程会位于等待队列中,是因为读写操作阻塞了;
- 该协程会同时位于两个等待队列中,是因为select有多路复用的机制,能够同时监听多个case,同一个协程进行多个读写操作阻塞,自然就会同时放在两个等待队列中;
- 假设读操作不阻塞的话,也就是其他协程中有对该管道写入数据,那么recvq等待队列里面就会把该协程移除,该协程就自然不会放在recvq等待队列中,select也就能够去随机执行一个case。
6、补充知识:select
- select功能:解决多个管道的选择问题,也可以叫多路复用,可以从多个管道中随机公平地选择一个来执行
- case后面必须进行的是io操作,不能是等值,随机去选择一个io操作
- default防止select被阻塞住,加入default
7、总结
- 在同一个协程中,select监听同一个channel的读和写,如果该channel有缓冲区,那么for循环下,select会随机执行读或写的case,一边取出一边放入,这样缓冲区就不会满了;
- 在同一个协程中,select监听同一个channel的写,没有读,如果该channel有缓冲区,那么for循环下,select会随机执行读或写的case,只有放入,这样缓冲区就会容易写满而导致写操作阻塞,最后只能执行default;
- 在同一个协程中,select监听同一个channel的读和写,如果该channel无缓冲区,那么此时管道既不能读也不能写,所以会直接输出default(如果没有default,select就会陷入阻塞,直到任意一个管道解除阻塞;有了default,select就是非阻塞的);
- 在同一个协程中,select监听同一个channel的写,没有读,如果该channel无缓冲区,此时管道也不能读,所以会直接输出default。
三、例子2
代码示例
var channels = [3]chan int{
nil,
make(chan int),
nil,
}
var numbers = []int{1, 2, 3}
func main() {
time.Sleep(time.Millisecond)
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.")
fmt.Println("")
case getChan(2) <- getNumber(2):
fmt.Println("The second candidate case is selected.")
fmt.Println("")
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.
select 语句的执行顺序
- 从上到下完成所有case后面的表达式
- case后面的表达式是从左往右依次执行
因此控制台会依次输出所有channels[]、numbers[] - 等所有表达式执行完成后,才会执行候选语句;
执行default,是因为case后面的表达式因为阻塞,而导致条件不成立。具体来说就是:第一个和第三个case管道值为nil,所以不管是读还是写管道,都会阻塞;第二个case是向无缓冲区的管道中写入数据,所以需要有其他协程来帮忙从管道中读数据,因为没有其他协程的帮忙,所以会阻塞。因而最终执行成功的case就只有default,其他的case都只是执行了case后面附带的表达式而已。
四、参考资料
- 《Go专家编程》
- 马士兵教育 【协程与管道】select功能