select
?是操作系统中的系统调用,我们以前在学校中学习操作系统课程或者在工作当中,肯定都使用过或者了解过?select
、poll
?和?epoll
?等函数构建 I/O 多路复用模型提升程序的性能。Go 语言的?select
?与操作系统中的?select
?很相似,今天这篇文章会深度解析?Go 语言?select
?关键字。
在Go语言中,select
语句用于处理多个通信操作,如通道操作。它允许我们等待多个操作完成,并根据条件执行相应的代码块。select
语句在并发编程中非常有用,特别是当我们需要处理多个通道操作时。
select
语句的语法结构如下:
select {
case <-channel1:
// 当channel1接收到数据时执行的代码块
case <-channel2:
// 当channel2接收到数据时执行的代码块
// ...
default:
// 如果没有任何通道接收数据时执行的代码块
}
select语句会一直监听所有指定的通道,直到其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好了,则select语句会随机选择一个通道执行。
如果没有任何通道准备好,则会执行default分支。
select
的通道操作,例如select <-channel
。非阻塞操作会立即返回,如果通道为空则继续执行下一个case
或default
。select
语句中使用,但通常与接收操作一起使用,以确保发送和接收之间的同步。发送操作可以在case
语句中使用,例如channel <- data
。<-channel
语法,用于从通道中读取数据。当通道接收到数据时,相应的case
代码块将被执行。如果没有任何通道接收到数据,将执行default
代码块或继续执行下一个case
。select
语句时,可以使用带有超时的通道操作来指定等待的时间限制。例如,可以使用带有超时的发送和接收操作,如下面代码。如果超过指定的时间没有数据可用,将执行相应的代码块或继续执行下一个case
。select {
case <-channel1:
timeout := time.After(2 * time.Second)
<-timeout
}
select
语句时,需要注意死锁问题。死锁通常发生在多个通道之间相互等待数据时,导致所有通道都无法继续执行。为了解决死锁问题,可以使用带有优先级的通道或使用其他并发控制机制来确保正确的同步和通信。下面是阻塞版的代码:?
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
?分支。