select 是操作系统中的系统调用,我们以前在学校中学习操作系统课程或者在工作当中,肯定都使用过或者了解过 selectpoll 和 epoll 等函数构建 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 main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string) // 创建一个字符串类型的无缓冲通道ch1
	ch2 := make(chan string) // 创建一个字符串类型的无缓冲通道ch2

	go func() { // 启动一个新的goroutine来模拟异步任务并发送数据到ch1通道中
		time.Sleep(2 * time.Second) // 休眠2秒以模拟异步任务耗时的情况
		ch1 <- "Hello from ch1"     // 向ch1通道发送数据"Hello from ch1"
	}() // 结束匿名函数并启动goroutine

	go func() { // 启动一个新的goroutine来模拟异步任务并发送数据到ch2通道中
		time.Sleep(1 * time.Second) // 休眠1秒以模拟异步任务耗时的情况
		ch2 <- "Hello from ch2"     // 向ch2通道发送数据"Hello from ch2"
	}() // 结束匿名函数并启动goroutine

	select { // 使用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 main

import (
	"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 <- 1
	ch2 <- 2

	time.Sleep(2 * time.Second)
}

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

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

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐