Go:23---select多路复用
程序员文章站
2022-06-13 12:37:48
...
一、一个例子(火箭发射程序)
- 假设现在我们还有一个这样的火箭发生倒计时程序:程序中创建了一个管道,管道定期发送数据,当接收到10次数据之后发射火箭
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Commencing countdown.")
// time.Tick返回一个管道, 管道命名为tick
tick := time.Tick(1 * time.Second)
// 循环从管道中接收10次数据
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
<- tick
}
// 循环完之后执行火箭发射函数
launch()
}
func launch() {
fmt.Println("Lift off!")
}
- 现在我们在该程序中新增一个功能:上面的程序没有中断功能,此时我们想添加一个功能,能够在倒计时的时候按下回车键来取消火箭的发射,改进后的代码如下所示,我们新建一个abort管道,当按下回车之后发送一个数据到abort管道中
// 创建一个管道
abort := make(chan struct{})
// 开启一个goroutine, 如果按下了回车键, 那么发送一个数据到abort管道中(下面介绍)
go func() {
os.Stdin.Read(make([]byte, 1))
abort <- struct{}{}
}
- 此处我们的程序中有2个管道,并且每个管道执行不同的功能,一个用来发送火箭,一个用来中止火箭发送。为了方便管理这些管道,我们可以使用select进行管理,见下面介绍
二、select简介
- select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个case必须是一个通信操作,要么是发送要么是接收
-
select随机执行一个可运行的case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的
语法格式
- select的格式如下:
select { case communication clause : statement(s); case communication clause : statement(s); /* 你可以定义任意数量的 case */ default : /* 可选 */ statement(s); }
- 相关语法:
- 每个case都必须是一个通信
- 所有channel表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行,其他被忽略
- 如果有多个case都可以运行,select 会随机公平地选出一个执行。其他不会执行
- 如果没有通道可以执行,则:
- 如果有default子句,则执行该语句
- 如果没有default子句,select 将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值
三、使用select重构火箭发射程序
实现1
- 有了select之后,我们可以将火箭发射程序进行重构,代码如下:程序中select检测两个管道,如果10秒内接收到abort的数据则终止程序,如果10秒内什么也没接收到,那么10秒之后time.After()返回一个管道数据,从而结束main函数
package main import ( "fmt" "time" "os" ) func main() { // 创建一个管道 abort := make(chan struct{}) // 开启后台goroutine, 如果从标准输入接收到数据, 向abort管道发送数据 go func() { os.Stdin.Read(make([]byte, 1)) abort <- struct{}{} }() fmt.Println("Commencing countdown. Press return to abort.") select { case <-time.After(10 * time.Second): // 10秒返回一个管道数据 // 什么也不做 case <-abort: // 如果接收到终止, 退出程序 return } launch() } func launch() { fmt.Println("Lift off!") }
- 如果10秒内不回车,那么程序正常结束,代表火箭发射成功
- 如果10秒内回车,那么程序终止,代表火箭发射终止:
实现2:打印倒计时
- 现在我们改写火箭发射程序,让其可以打印倒计时。代码如下:
package main import ( "fmt" "time" "os" ) func main() { // 中止管道 abort := make(chan struct{}) go func() { os.Stdin.Read(make([]byte, 1)) abort <- struct{}{} }() // tick为一个管道 tick := time.Tick(1 * time.Second) fmt.Println("Commencing countdown. Press return to abort.") for countdown := 10; countdown > 0; countdown-- { // 打印信息 fmt.Println(countdown) select { case <-tick: // 什么也不做 case <-abort: return } } launch() } func launch() { fmt.Println("Lift off!") }
goroutine泄漏(time.Tick()与time.NewTicker())
- time.Tick()可能会带来goroutine泄漏,time.Tick()函数创建了一个goroutine在循环里面调用time.Sleep,当time.Sleep返回时向管道发送一个数据
- 在上面的"实现2"代码中,如果上面的代码不是运行在一个main函数里面,假设运行在funcTest()函数中,其中tick就是一个管道,当函数funcTest()结束之后,time.Tick()后台创建的goroutine还没有结束,而funcTest()函数已经结束,那么就没有人能从goroutine中接收数据了,导致后台goroutine一直阻塞运行,此时造成goroutine泄漏
func funcTest() { // time.Tick后台会创建一个goroutine tick := time.Tick(1 * time.Second) for countdown := 10; countdown > 0; countdown-- { fmt.Println(countdown) select { case <-tick: // 什么也不做 case <-abort: return } } } // 当funcTest函数结束之后, time.Tick()创建的goroutine仍在运行, 但是没有人从该goroutine中创建的管道上接收数据了, 造成goroutine泄漏
- 一个解决办法就是使用time.NewTicker()函数
ticker := time.NewTicker(1 * time.Second) <-ticker.C // 从管道中接收数据 ticker.Stop() //不使用时可以关闭, 后台goroutine也结束
- 因为time.Tick()函数适用于整个生命周期中都要使用它的情况,否则就使用上面的接口
四、演示案例
- 下面的例子中用到了缓冲通道,通道ch的缓冲区大小为1,它要么是空的,要么是慢的,因此只有在其中一个状况下可以执行:i是偶数时发送,i是奇数时接收
- 代码如下:
package main
import "fmt"
func main() {
// 创建一个缓冲区大小为1的管道
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch: // 从管道中接收数据
fmt.Println(x)
case ch <- i: // 向管道发送数据
}
}
}
五、select的非阻塞操作(default)
- 在使用select的时候,我们不想在通道没有准备好的情况下被阻塞——非阻塞通信,这个时候可以使用sleect的default语句
- 例如,下面的代码尝试从abort通道上接收一个值,如果什么也没有则什么也不做,重复对通道的轮询
select {
case <-abort:
fmt.Println("Launch aborted!")
return
default:
// 什么也不做
}
六、nil通道
- 通道的零值是nil
- 令人惊讶的是,nil通道有时候很有用。因为在nil通道上发送和接收将永远阻塞
- 对于select语句中的情况,如果其通道是nil,它将永远不会被选择
- 这次让我们用nil来开启或禁用特性所对应的情况,比如超时处理或者取消操作,响应其他的输入事件或者发送时间
- 在下面的"并发目录遍历"演示案例中使用到了nil通道,见下面代码
七、演示案例(并发目录遍历)
- 待续
上一篇: 大数据开发面试题
下一篇: Java基础Day06