Go 并发编程 (一) Select

我觉得 Go 并发编程和其他语言差别很大,其原因在于它有自己独特的设计。Select 就是其中一个很特别的设计,它能在并发中起到什么样的效果呢?

select {
case 语句1:
    ...
case 语句2:
    ...
default:
    ...
}

乍一看 select 语句的长得有点像 switch,但它跟 switch 完全不同,不仅语法上存在不同,二者的使用场景也完全不同

select 有什么特别之处呢?

select 的使用

举一个具体使用的例子:

select { // 不停的监听每个 case 语句中 channel 的 IO 操作

case <-chanl : //检测有没有数据可以读
// 如果chanl成功读取到数据,则进行该case处理语句

case chan2 <- 1 : //检测有没有可以写
// 如果成功向chan2写入数据,则进行该case处理语句

default:
// default 子句总是可运行的
}

select 主要用于处理异步通道操作,这使得它在语法上有一些特别:

  • case 语句必须包含一个 channel 操作,必须是一个通信(IO)
  • default 子句总是可运行的
  • select 没有条件表达式,一直在等待分支进入可运行状态

select 在执行上也很特别,在一个 select 语句中,Go 会按顺序监听每一个 case 语句中的 channel 的读写操作:

  • 如果有多个分支都可以继续执行(即没有被阻塞),select 会随机地选一个执行,其他分支不会执行
  • 如果没有可运行的分支(即所有通道都被阻塞),那么有两种情况:
    • 有 default 语句,那么就会执行 default 的动作
    • 没有 default 语句,select 将阻塞,直到某个分支可以运行

就比如上面代码中的例子,

  • 如果 chan1 有数据可读,且 chan2 可以写数据,那么会随机地选一个 case 处理
  • 如果那个 case 都不能继续执行,那么会执行 default 中的处理语句

select 的使用场景

我们初步认识到 select 是用于处理异步通道操作,那么它在具体能用于哪些场景呢?

1. 超时处理

使用 select + 定时器可以实现:给通道增加读写数据的容忍时间,如果超过限定的时间内无法读写,就停止处理

var ch = make(chan int)
func test() {
    select{
    case data := <-ch:
        doSometing(data)
    case <-time.After(time.Second * 5):
        fmt.Println("request time out")    
}

2. 判断 channel 是否阻塞

在某些情况下,想判断 channel 缓存是否满了

ch := make(chan int, 1024)
data := 0

select{
case ch <- data:
default:
    doSometing()

PS: 暂且就知道以上这几种使用场景,之后碰上了会再补上来

Select 的性能问题

因为 select 并不是采用事件触发,所以当 chanel 数量超过 1 时,Go 内部会创建一个 goroutine,由它每秒轮询的方式检查每个 channel 是否可用

这导致了 select 异步操作随着 channel 数增加,性能线性下降,每增加一个管道增加 100ns/op

解决的方法是,在使用 select 时应避免使用过多的管道 chan 分支,或者把无法用到的 chan 置为 nil

参考了以下文章:
刘鹏杰的 Golang 文档