golang学习随便记11
Posted sjg20010414
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang学习随便记11相关的知识,希望对你有一定的参考价值。
goroutine 和 channel (3)
基于 select 的 多路复用
之前我们的程序中,等待 channel 发送过来信号,都是单一的 channel,和写多线程程序一样,还存在一种可能,我们等待多个信号中的一个发生就要“采取行动”,这就是多路信号(多路事件)——原文 multiplex,被翻译成多路复用。
书中的例子是火箭发射:正常状态每一秒发送一个tick信号,但突发异常时,操作员可以按下return键中断发射(发送abort信号)
先看一个不带 abort 功能的火箭发射倒计时
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Commencing countdown.")
tick := time.Tick(1 * time.Second) // 产生一个 time.Time 型 channel 并自动定时发送信号
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
<-tick // 接收信号 (信号值是时间戳)
}
launch()
}
func launch() {
fmt.Println("The Rocket launched")
}
带 abort 功能的
package main
import (
"fmt"
"os"
"time"
)
func main() {
abort := make(chan struct{})
go func() { // 开启独立 goroutine
os.Stdin.Read(make([]byte, 1)) // 从标准输入读入1字节
abort <- struct{}{} // 发送 abort 信号
}()
fmt.Println("Commencing countdown.")
select {
case <-time.After(10 * time.Second): // 接收到了时间戳 (对应 timeout 信号)
// do nothing
case <-abort: // 接收到了 abort 信号
fmt.Println("Launch aborted!")
return
}
launch()
}
func launch() {
fmt.Println("The Rocket launched")
}
上面的两个程序中,time.Tick 和 time.After 稍微有点烧脑。这两个函数都会返回 time.Time类型的channel,也就是说这两个函数都有 make(chan time.Time) 的功效,同时,time.Tick 在背后自动每秒发送一个信号到 channel,而 time.After 则在设定的时间到了之后背后自动往 channel 发送一个信号,time.Tick 类似 JS 里面的 setInterval,而 time.After 则类似 setTimeout。<-time.After(10*time.Second) 要分两步来理解,即 ch := time.After(10*time.Second) 和 <-ch
下面的程序能更好说明 multiplex 中多个信号有一个信号满足就可以的特点:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case x := <-ch:
fmt.Println(x)
case ch <- i:
}
}
}
输出
0
2
4
6
8
我们来梳理一下程序执行流程:ch 是一个内部队列缓冲为1个元素的 channel,当 i 为 0 时,ch 内还没有元素(队列为空),所以,<-ch 会阻塞,ch <- i 得到满足,发送 0 到 channel 的队列,队列变满。当 i 为 1 时,如果 channel 缓冲队列中的元素还没有被接收,那么 ch<- i 肯定会阻塞,而 <-ch 可以得到满足 (取出 0 赋值给 x),队列中的元素被接收,队列再次变空。当 i 为 2 时,又回到和 i 为 0 时的状态,从而整个过程队列交替为空或为满。
select 只要求有一个 case 得到满足就行,而如果多个case同时就绪,select会随机选择一个执行,这样来保证每一个channel都有平等被select的机会。在上面的例子中,把队列大小改成大于1的值,出来的结果就随机了,因为队列既不空也不满,两个case都满足,select语句的执行就像抛硬币的行为一样。
前面的两个火箭发射程序,前一个能每隔一秒打印倒计时数,但不能终断发射,后一个能够中断发射,但倒计时的时候,不能打印倒计时数。我们把它改成既能终断又能打印倒计时数。
package main
import (
"fmt"
"os"
"time"
)
func main() {
abort := make(chan struct{})
go func() { // 开启独立 goroutine
os.Stdin.Read(make([]byte, 1)) // 从标准输入读入1字节
abort <- struct{}{} // 发送 abort 信号
}()
fmt.Println("Commencing countdown. Press return to abort")
tick := time.Tick(1 * time.Second) // 每一秒发送 tick
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-tick: // 接收到了 tick
// do nothing
case <-abort: // 接收到了 abort 信号
fmt.Println("Launch aborted!")
return
}
}
launch()
}
func launch() {
fmt.Println("The Rocket launched")
}
从输出结果来看,程序没有问题。问题是在于,time.Tick 会自动创建一个 goroutine 并向 channel 每一秒发送一个信号(时间戳),即使 launch 已经执行,只要程序没有结束,时间戳还在发送(gouroutine leak),所以,只有全局生命周期的场合才可能适用 time.Tick。对于局部的定时场合,应该使用 time.NewTicker 创建 channel 和后台 goroutine 来发送信号,并且不需要时明确终结后台 goroutine:
ticker := time.NewTicker(1 * time.Second)
<-ticker.C // receive from the ticker's channel
ticker.Stop() // cause the ticker's goroutine to terminate
也就是把程序改成如下形式:
package main
import (
"fmt"
"os"
"time"
)
func main() {
abort := make(chan struct{})
go func() { // 开启独立 goroutine
os.Stdin.Read(make([]byte, 1)) // 从标准输入读入1字节
abort <- struct{}{} // 发送 abort 信号
}()
fmt.Println("Commencing countdown. Press return to abort")
tick := time.NewTicker(1 * time.Second) // 每一秒发送 tick
for countdown := 10; countdown > 0; countdown-- {
fmt.Println(countdown)
select {
case <-tick.C: // 接收到了 tick
// do nothing
case <-abort: // 接收到了 abort 信号
tick.Stop() // 终结后台 tick goroutine
fmt.Println("Launch aborted!")
return
}
}
tick.Stop() // 终结后台 tick goroutine
launch()
}
func launch() {
fmt.Println("The Rocket launched")
}
前面的 select 用法中,我们总是等待某个 case 得到满足,而我们的 case,不是从 channel 接收值,就是向 channel 发送值,即,如果 case 中的 channel 都是没有准备好(写或读)的,是会产生阻塞的。在另一些场合,我们希望其它操作不能马上被处理时,程序做某些工作,这可以通过 select 语句 的 default 分支来实现。
select {
case <-abort:
fmt.Printf("Launch aborted!\\n")
return
default:
// do nothing
}
上面的接收操作是不会阻塞的,因为没有接收到信号,default 分支会执行。这样的结构放入循环,就变成 轮询 channel 。
channel 的零值是 nil,nil 的 channel 对于调试是有用的,因为向 nil channel 发送 或 从 nil channel 接收都会阻塞,这样,将 select 某个case分支的 channel 置为 nil,它就永远不会被选中,相当于屏蔽了这一分支。
以上是关于golang学习随便记11的主要内容,如果未能解决你的问题,请参考以下文章