Go 学习笔记(二十五)双层 channel 用法示例
本文原创地址:博客园骏马金龙Go 基础系列:双层 channel 用法示例
以下是一个双层通道的使用示例。注意下面的示例中使用了 "信号通道"(Signal channel),但这里的信号通道是多余的,仅仅只是为了介绍。
信号通道不用来传递数据,而是用来传递消息,用来产生可读、可写的事件,以便让 select 选中某个分支。产生消息事件的方式有多种,比如直接关闭通道、发送 false/true 布尔值等等 。
package main
import (
"fmt"
"time"
)
func main() {
// 定义双层通道cc
cc := make(chan chan int)
times := 5
for i := 1; i < times+1; i++ {
// 定义信号通道f
f := make(chan bool)
// 每次循环都在双层通道cc中生成内层通道c
// 并通过信号通道f来终止f1()
go f1(cc, f)
// 从双层通道cc中取出内层通道ch
// 并向ch通道发送数据
ch := <-cc
ch <- i
// 从ch中取出数据
for sum := range ch {
fmt.Printf("Sum(%d)=%d\n", i, sum)
}
// 每个循环睡眠一秒钟
time.Sleep(time.Second)
// 每次循环都关闭信号通道f
close(f)
}
}
// 双层通道cc用来生成内层通道c
// 并使用信号通道f来终止函数f1()
func f1(cc chan chan int, f chan bool) {
c := make(chan int)
cc <- c
defer close(c)
sum := 0
select {
// 从内层通道中取出数据,计算和,然后发回内层通道
case x := <-c:
for i := 0; i <= x; i++ {
sum = sum + i
}
// goroutine将阻塞在此,直到数据被读走
c <- sum
// 信号通道f可读时,结束f1()的运行
// 但因为select没有在for中,该case分支用不上
case <-f:
return
}
}
上面的示例中,函数 f1()两个参数,一个是双层通道 cc,一个是信号通道 f。f1() 中首先生成了一个通道 c,并发送给了双层通道 cc,使得 main() 中可以从 cc 中取得这个内层通道 c,并向其发送数据。
回到 f1()中,select 最初会被阻塞,因为内层通道 c 和信号通道 f 都没有数据可读。由于 main() 可以取得内层通道 c,并向其发送数据,使得 f1()中的 select 第一个 case 分支被选中,该分支会计算发送的整数之前的总和,并将计算结果重新发送给内层通道 c,让 main() 可以取得这个计算结果。
上面的示例中有几个细节需要注意:
- 在 f1()中必须关闭内层通道 c,因为 main() 中的 range 迭代一个未关闭的通道会一直阻塞,而且每次调用 f1() 都会重新创建 c 通道。
- 上面的信号通道其实没有起到任何作用。
- f1()中的 select 必须不能放进 for 循环。因为 f1() 将数据发回 c 之后,如果在 for 中,发 f() 所在的 goroutine 将阻塞在 select 上,由于 c 通道还没有关闭,这会导致 main goroutine 因 range 迭代操作而阻塞,也就是说所有 goroutine 都被阻塞了,出现了死锁。
所以,当在 select 中有发送操作的时候,很可能会出现死锁现象。这时, 要么为 select 加上 default,要么为 select 加上超时时间,要么 select 不要放在 for 循环中 。
上一篇 Go 学习笔记(二十四)channel 入门
Go 学习笔记(目录)
下一篇 Go 学习笔记(二十六)指定 goroutine 的执行顺序