多路复用select

* 本页面主要介绍Go语言中基于select实现的多路复用的相关内容。

首先,在学习select之前,需要明确的是你已经熟悉并掌握了 通道channel 这一节的全部知识点。否则,最好回去重新学习或者复习一样,因为本节的select是基于通道的一种特殊技能。

那么,我们就开始学习这个神奇的select!

select是啥?

select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个基于channel的通信操作,要么是发送要么是接收。select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

讲了这么多,看个例子:

            
  // 准备好几个通道。
  intChannels := [3]chan int{
    make(chan int, 1),
    make(chan int, 1),
    make(chan int, 1),
  }

  // 随机选择一个通道,并向它发送元素值。
  index := rand.Intn(3)
  fmt.Printf("The index: %d\n", index)

  intChannels[index] <- index

  // 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
  select {
  case <-intChannels[0]:
    fmt.Println("The first candidate case is selected.")
  case <-intChannels[1]:
    fmt.Println("The second candidate case is selected.")
  case elem := <-intChannels[2]:
    fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
  default:
    fmt.Println("No candidate case is selected!")
  }
            

select语句的分支分为两种,一种叫做候选分支,另一种叫做默认分支

如果像上述示例那样加入了默认分支,那么无论涉及通道操作的表达式是否有阻塞,select语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。

如果没有加入默认分支,那么一旦所有的case表达式都没有满足求值条件,那么select语句就会被阻塞。直到至少有一个case表达式满足条件为止。

在很多时候,我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。这对于程序逻辑和程序性能都是有好处的。

select语句只能对其中的每一个case表达式各求值一次

一个没有任何case的select语句写作select{},会永远地等待下去

因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。这使得我们可以用nil来激活或者禁用case,来达成处理其它输入或输出事件时超时和取消的逻辑。

select的分支选择规则是啥样的?
  • 对于每一个case表达式,都至少会包含一个代表发送操作的发送表达式或者一个代表接收操作的接收表达式,同时也可能会包含其他的表达式。多个表达式总会以从左到右的顺序被求值。
  • select语句包含的候选分支中的case表达式都会在该语句执行开始时先被求值,并且求值的顺序是依从代码编写的顺序从上到下的。
  • 对于每一个case表达式,如果其中的发送表达式或者接收表达式在被求值时,相应的操作正处于阻塞状态,那么对该case表达式的求值就是不成功的。也就是不满足选择条件。
  • 仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么select语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select语句(或者说它所在的 goroutine)就会被唤醒,这个候选分支就会被执行。
  • 一条select语句中只能够有一个默认分支。并且,默认分支只在无候选分支可选时才会被执行,这与它的编写位置无关。
  • select语句的每次执行,包括case表达式求值和分支选择,都是独立的。不过,至于它的执行是否是并发安全的,就要看其中的case表达式以及分支中,是否包含并发不安全的代码了。

下面这个程序就是利用channel的多路复用实现火箭发射的倒数程序:


func main() {
    
    abort := make(chan struct{})
    go func() {
        os.Stdin.Read(make([]byte, 1)) // read a single byte
        abort <- struct{}{}
    }()

    fmt.Println("Commencing countdown.  Press return to abort.")
    select {
    case <-time.After(10 * time.Second):
        // Do nothing.
    case <-abort:
        fmt.Println("Launch aborted!")
        return
    }

    launch()
}
            

关于select的内容就总结这么多,你GET了吗?


* 本页内容参考以下数据源:

  • 《Go程序设计语言》
  • 《Go语言核心36讲》 11 通道的高级玩法
  • http://books.studygolang.com/gopl-zh/ch8/ch8-07.html

凯冰科技 · 代码改变世界,技术改变生活
Next Page→