Golang中的select语句是控制并发的利器,可以作为多路复用器同时阻塞并等待多个发送或接收操作,它包含以下特性:

  • 在select中任意一个操作被解除阻塞之前,整个select语句作为一个整体阻塞。

  • 如果select中有多个case可以执行,那么将随机选择其中一个执行。

 

以下是select语句的一般形式。和switch语句稍微有点相似,也会有几个case和default分支。每一个case代表一个通信操作,可以在某个channel上进行发送或者接收,并且会包含一些语句组成的一个语句块。一个接收表达式可能只包含接收表达式自身,比如第一个case,或者包含在一个简短的变量声明类似第二个case一样;第二种形式能够引用接收到的值。

select {
case <-channel1:
    // ...
case x := <-channel2:
    // ...使用变量x
case channel3 <- y:
    // ...
default:
    // ...
}

select将会一直等待,直到case中有某个条件能够执行为止。当条件满足时,select才会去某个channel中取数据并执行case之后的语句;这时候其它channel是不会执行的。一个没有任何case的select语句写作select{},将会永远地阻塞。

 

// 在ch1或ch2通道上有可用数据为止,以下处理逻辑将始终保持阻塞。

select{

case<-ch1:

        fmt.Println("Receivedfrom ch1")

case<-ch2:

        fmt.Println("Receivedfrom ch2")

}

如果在nil通道上执行发送或接收操作将导致永久阻塞。这可以用来禁用select语句中的某些通道,如下所例:

ch1= nil // 通过设置为nil来禁用该通道

select{

case<-ch1:

        fmt.Println("Receivedfrom ch1") // 永远不会被执行

case<-ch2:

        fmt.Println("Receivedfrom ch2")

}

 

Default分支

如果所有其他case都被阻塞,则默认的default分支始终能够继续运行。样例如下:

// 以下代码逻辑永远不会被阻塞

select{

casex := <-ch:

        fmt.Println("Received",x)

default:

        fmt.Println("Nothingavailable")

}

 

多路复用器select示例

示例1:无限随机二进制序列

作为一个简单的游戏示例,以下代码可以基于select随机选择的特性产生0和1的随机二进制序列。

rand:= make(chan int, largeBuffer)

for{

    select {

        caserand <- 0: // no statement

        caserand <- 1:

    }

}

 

示例2:带有超时的阻塞操作

函数time.After是标准库的一部分;它将会等待指定的时间,然后在返回的通道上发送当前时间。

select{

casenews := <-AFP:

        fmt.Println(news)

case<-time.After(time.Minute):

        fmt.Println("Timeout: No news in one minute")

}

 

示例3:一个永远阻塞的语句

以下的select语句将永远阻塞,因为没有任务case可以继续向下执行。

select{}

在一些多线程程序中,通常是用在main函数的末尾。当main返回时,程序退出。

 

总结

本质上select的case语句,都是对应一个channel的I/O操作。select思想来源于网络IO模型,本质上也是IO多路复用,只不过这里的IO是基于channel的。在使用select机制时需要注意一些关键点,比如default子句是否需要设置,是否对通道关闭的情况进行了合理配置,是否透彻地理解了select的随机性等。

 

select的特性再次回顾:

  • 每个case语句中必须包含或指向一个通道。

  • 每个case语句中针对通道的表达式都会被求值。

  • 每个case中所有向通道发送数据的表达式都会被求值。

  • 当某个case中的通道可用时,将会执行对应的statement,此时其他case语句将被忽略。

  • 当存在多个case都可以运行时,select会随机地选择一个分支执行。未被选中的分支将不会被执行。

  • 如果没有任务case可以执行,并且分支中存在default子句,此时执行default分支。

  • 如果没有default字句,整个select将阻塞,直到某个通道可用为止。此外,Go不会重新对通道进行求值。