golang学习随便记11-goroutine和channel(3)

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.Ticktime.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,它就永远不会被选中,相当于屏蔽了这一分支。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值