select多路复用

TrumanWong
9/23/2024
TrumanWong

谈到select,很多读者会想到网络编程中用于I/O多路复用的select函数,在Go语言中,select语句有类似的功能。

在使用通道时,更多的时候会与select结合,因为时常会出现多个通道与多个协程进行通信的情况,我们当然不希望由于一个通道的读写陷入堵塞,影响其他通道的正常读写。select正是为了解决这一问题诞生的,select赋予了Go语言更加强大的功能。在使用方法上,select的语法类似switch,形式如下:

select {
    case <-ch1:
    // ...
    case x := <-ch2:
        // ...
    case ch3 <- y:
    // ...
    default:
        // ...
}

switch不同的是,每个case语句都必须对应通道的读写操作。select语句会陷入堵塞,直到一个或多个通道能够正常读写才恢复。

select堵塞与控制

如果select中没有任何的通道准备好,那么当前select所在的协程会永远陷入等待,直到有一个case中的通道准备好为止:

ch := make(chan int, 1)
for {
    select {
    case <-ch:
        fmt.Println("random 01")
    case <-ch:
        fmt.Println("random 02")
    }
}

在实践中,为了避免这种情况发生,有时会加上default分支。default分支的作用是当所有的通道都陷入堵塞时,正常执行default分支:

ch := make(chan int, 1)
for {
    select {
    case <-ch:
        fmt.Println("random 01")
    case <-ch:
        fmt.Println("random 02")
    default:
        fmt.Println("default")
    }
}

除了default,还有一些其他的选择。例如,如果我们希望一段时间后仍然没有通道准备好则超时退出,可以选择select与定时器或者超时器配套使用。

如下所示,<-time.After800*time.Millisecond)调用了time包的After函数,其返回一个通道800ms后会向当前通道发送消息,可以通过这种方式完成超时控制:

for {
    select {
    case <-ch:
        fmt.Println("random 01")
    case <-ch:
        fmt.Println("random 02")
    case <-time.After(800 * time.Millisecond):
        fmt.Println("timeout")
    }
}

循环select

很多时候,我们不希望select执行完一个分支就退出,而是循环往复执行select中的内容,因此需要将forselect进行组合,如上面的示例所示。

forselect组合后,可以向select中加入一些定时任务。下例中的tick每隔1s就会向tick通道中写入数据,从而完成一些定时任务:

func main() {
    ch := make(chan int, 1)
    tick := time.Tick(500 * time.Millisecond)
    for {
        select {
        case <-ch:
            fmt.Println("random")
        case <-tick:
            fmt.Println("tick")
        case <-time.After(800 * time.Millisecond):
            fmt.Println("default")
        }
    }
}
需要注意的是,定时器time.Ticktime.After是有本质不同的:time.After并不会定时发送数据到通道中,而只是在时间到了后发送一次数据。当其放入for+select后,新一轮的select语句会重置time.After,这意味着第2次select语句依然需要等待800ms才执行超时。如果在800ms之前,其他的通道就已经执行好了,那么time.Aftercase将永远得不到执行。而定时器tick不同,由于tickfor循环的外部,因此其不重置,只会累积时间,实现定时执行任务的功能。

selectnil

一个为nil的通道,不管是读取还是写入都将陷入堵塞状态。select语句的casenil通道进行操作时,case分支将永远得不到执行

nil通道的这种特性,可以用于设计一些特别的模式。例如,假设有ch1ch2两个通道,我们希望交替地向ch1ch2通道中发送消息,那么可以用如下方式:

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go func() {
        for i := 0; i < 2; i++ {
            select {
            case ch1 <- 1:
                ch1 = nil
            case ch2 <- 2:
                ch2 = nil
            }
        }
    }()
    fmt.Println(<-ch1)
    fmt.Println(<-ch2)
}

一旦写入通道后,就将该通道置为nil,导致再也没有机会执行该case。从而达到交替写入ch1ch2通道的目的。