select是操作系统中的系统调用,我们以前在学校中学习操作系统课程或者在工作当中,肯定都使用过或者了解过selectpollepoll等函数构建 I/O 多路复用模型提升程序的性能。Go 语言的select与操作系统中的select很相似,今天这篇文章会深度解析Go 语言select关键字。

在Go语言中,select语句用于处理多个通信操作,如通道操作。它允许我们等待多个操作完成,并根据条件执行相应的代码块。select语句在并发编程中非常有用,特别是当我们需要处理多个通道操作时。

语法相关

select语句的语法结构如下:

select {case <-channel1:// 当channel1接收到数据时执行的代码块case <-channel2:// 当channel2接收到数据时执行的代码块// ...default:// 如果没有任何通道接收数据时执行的代码块}

select语句的执行

select语句会一直监听所有指定的通道,直到其中一个通道准备好就会执行相应的代码块。

如果多个通道都准备好了,则select语句会随机选择一个通道执行。

如果没有任何通道准备好,则会执行default分支。

select语句的注意事项

  • select语句只能用于通道操作,每个case语句必须是一个通道操作。
  • select语句会一直监听所有指定的通道,直到其中一个通道准备好就会执行相应的代码块。
  • 如果多个通道都准备好了,则select语句会随机选择一个通道执行。
  • 如果没有任何通道准备好,则会执行default分支(注意:如果没有default,程序则会陷入阻塞)。

深度解析

非阻塞与阻塞操作
  • 非阻塞操作:默认情况下,通道操作是阻塞的,即它会等待数据可用。如果要进行非阻塞操作,可以使用带有select的通道操作,例如select <-channel。非阻塞操作会立即返回,如果通道为空则继续执行下一个casedefault
  • 阻塞操作:当通道被阻塞时,它会一直等待直到有数据可用。这通常用于同步操作,确保发送和接收之间的正确匹配。阻塞操作可以用于确保数据的完整性和顺序。
发送与接收操作
  • 发送操作:除了通道接收操作外,还可以使用通道发送操作。发送操作可以在select语句中使用,但通常与接收操作一起使用,以确保发送和接收之间的同步。发送操作可以在case语句中使用,例如channel <- data
  • 接收操作:通道接收操作使用<-channel语法,用于从通道中读取数据。当通道接收到数据时,相应的case代码块将被执行。如果没有任何通道接收到数据,将执行default代码块或继续执行下一个case
超时与抢占
  • 超时:在使用select语句时,可以使用带有超时的通道操作来指定等待的时间限制。例如,可以使用带有超时的发送和接收操作,如下面代码。如果超过指定的时间没有数据可用,将执行相应的代码块或继续执行下一个case
select {case <-channel1:timeout := time.After(2 * time.Second)<-timeout}
  • 抢占:在某些情况下,可能希望中断当前阻塞的通道操作并执行其他代码。Go语言的运行时支持抢占机制,可以用于中断阻塞的操作。当一个goroutine被抢占时,它会立即停止当前的操作并执行其他代码。这有助于避免死锁和饥饿问题。
死锁与饥饿问题
  • 死锁:在使用select语句时,需要注意死锁问题。死锁通常发生在多个通道之间相互等待数据时,导致所有通道都无法继续执行。为了解决死锁问题,可以使用带有优先级的通道或使用其他并发控制机制来确保正确的同步和通信。
  • 饥饿问题:饥饿问题发生在某些情况下,某些通道总是得不到执行的机会。为了避免饥饿问题,可以使用公平调度策略或限制每个通道的执行时间。此外,使用缓冲通道可以帮助平衡多个goroutine之间的通信和数据传输。

代码案例

下面是阻塞版的代码:

package mainimport ("fmt""time")func main() {ch1 := make(chan string) // 创建一个字符串类型的无缓冲通道ch1ch2 := make(chan string) // 创建一个字符串类型的无缓冲通道ch2go func() { // 启动一个新的goroutine来模拟异步任务并发送数据到ch1通道中time.Sleep(2 * time.Second) // 休眠2秒以模拟异步任务耗时的情况ch1 <- "Hello from ch1" // 向ch1通道发送数据"Hello from ch1"}() // 结束匿名函数并启动goroutinego func() { // 启动一个新的goroutine来模拟异步任务并发送数据到ch2通道中time.Sleep(1 * time.Second) // 休眠1秒以模拟异步任务耗时的情况ch2 <- "Hello from ch2" // 向ch2通道发送数据"Hello from ch2"}() // 结束匿名函数并启动goroutineselect { // 使用select语句等待任意一个通道接收到数据case msg1 := <-ch1: // 当ch1通道接收到数据时,将数据赋值给msg1变量并执行该case分支的代码块fmt.Println("Received from ch1:", msg1) // 打印接收到的数据case msg2 := <-ch2: // 当ch2通道接收到数据时,将数据赋值给msg2变量并执行该case分支的代码块fmt.Println("Received from ch2:", msg2) // 打印接收到的数据}}

这里是非阻塞版的代码

package mainimport ("fmt""time")func main() {ch1 := make(chan int)ch2 := make(chan int)go func() {for {select {case v := <-ch1:fmt.Println("Received", v, "from ch1")case v := <-ch2:fmt.Println("Received", v, "from ch2")case <-time.After(1 * time.Second):fmt.Println("Timeout")}}}()time.Sleep(2 * time.Second)ch1 <- 1ch2 <- 2time.Sleep(2 * time.Second)}

这段代码与之前的代码相比,主要有以下几个改动:

  • select语句使用了time.After(1 * time.Second)超时。这意味着,如果在 1 秒内没有通道有可用的数据,那么select语句会立即返回,并执行Timeout语句。
  • 即使在ch1ch2都没有可用的数据时,select语句也不会阻塞,而是会在 1 秒后返回,并执行Timeout语句。
  • 注意上面的select语句没有default分支,但它仍然是非阻塞的。这是因为,time.After(1 * time.Second)超时本身就相当于一个default分支。如果在 1 秒内没有通道有可用的数据,那么select语句会立即返回,并执行Timeout语句。因此,在使用超时实现非阻塞的select语句时,不需要加default分支。