Go 并发编程|select语句(IO多路复用)

2,162 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

引言

select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select是Golang在语言层面提供的多路IO复用的机制,其可以检测多个channel是否ready(即是否可读或可写),使用起来非常方便。

1. 语法结构

select语句的语法结构和switch语句很相似,也有case语句和default语句:

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

说明:

  • 每个case都必须是一个通信

  • 所有channel表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

2. 示例代码

示例代码:

package main

import (
    "fmt"
    "time"
)

func main() {
    /*
    分支语句:if,switch,select
    select 语句类似于 switch 语句,
        但是select会随机执行一个可运行的case。
        如果没有case可运行,它将阻塞,直到有case可运行。
     */

    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- 200
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch1 <- 100
    }()

    select {
    case num1 := <-ch1:
        fmt.Println("ch1中取数据。。", num1)
    case num2, ok := <-ch2:
        if ok {
            fmt.Println("ch2中取数据。。", num2)
        }else{
            fmt.Println("ch2通道已经关闭。。")
        }

    }
}

运行结果:

可能执行第一个case,打印100,也可能执行第二个case,打印200。(多运行几次,结果就不同了)

select语句结合time包的和chan相关函数

示例代码:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    //go func() {
    //  ch1 <- 100
    //}()

    select {
    case <-ch1:
        fmt.Println("case1可以执行。。")
    case <-ch2:
        fmt.Println("case2可以执行。。")
    case <-time.After(3 * time.Second):
        fmt.Println("case3执行。。timeout。。")

    //default:
    //  fmt.Println("执行了default。。")
    }
}

运行结果:

image.png

3. 实现原理

Golang实现select时,定义了一个数据结构表示每个case语句(含defaut,default实际上是一种特殊的case),select执行过程可以类比成一个函数,函数输入case数组,输出选中的case,然后程序流程转到选中的case块。

3.1 case数据结构

源码包src/runtime/select.go:scase定义了表示case语句的数据结构:

type scase struct {    
    c           *hchan         // chan    
    kind        uint16    
    elem        unsafe.Pointer // data element
}

scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。scase.kind表示该case的类型,分为读channel、写channel和default,三种类型分别由常量定义:

  • caseRecv:case语句中尝试读取scase.c中的数据;
  • caseSend:case语句中尝试向scase.c中写入数据;
  • caseDefault: default语句

scase.elem表示缓冲区地址,跟据scase.kind不同,有不同的用途:

  • scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址;
  • scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;

3.2 select实现逻辑

源码包src/runtime/select.go:selectgo()定义了select选择case的函数:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

函数参数:

  • cas0为scase数组的首地址,selectgo()就是从这些scase中找出一个返回。

  • order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder

    • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。
    • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。
  • ncases表示scase数组的长度

函数返回值:

  1. int: 选中case的编号,这个case编号跟代码一致
  2. bool: 是否成功从channle中读取了数据,如果选中的case是从channel中读数据,则该返回值表示是否读取成功。

selectgo实现伪代码如下:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {    
    //1. 锁定scase语句中所有的channel    
    //2. 按照随机顺序检测scase中的channel是否ready    
    //   2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true)     //   2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false)     //   2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false)    
    //3. 所有case都未ready,且没有default语句    
    //   3.1 将当前协程加入到所有channel的等待队列     
    //   3.2 将协程转入阻塞,等待被唤醒    
    //4. 唤醒后返回channel对应的case index    
    //   4.1 如果是读操作,解锁所有的channel,然后返回(case index, true)    
    //   4.2 如果是写操作,解锁所有的channel,然后返回(case index, false)}

特别说明:对于读channel的case来说,如case elem, ok := <-chan1:, 如果channel有可能被其他协程关闭的情况下,一定要检测读取是否成功,因为close的channel也有可能返回零值,此时ok == false

4. 总结

  • select语句中除default外,每个case操作一个channel,要么读要么写
  • select语句中除default外,各case执行顺序是随机的
  • select语句中如果没有default语句,则会阻塞等待任一case
  • select语句中读操作要判断是否成功读取,关闭的channel也可以读取