vault backup: 2026-06-07 12:14:39
This commit is contained in:
+128
-160
@@ -1,190 +1,158 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go进阶
|
||||
- select
|
||||
- 并发
|
||||
tags: [go, golang, Select, 并发, 多路复用]
|
||||
create time: 2026-06-07 14:45
|
||||
---
|
||||
|
||||
# Select
|
||||
|
||||
## select是什么
|
||||
`select`是Go语言层面提供的一种多路复用机制,用于检测当前`goroutine`连接的多个`channel`是否有数据准备完毕,可用于读或写。
|
||||
## 概述
|
||||
|
||||
## IO多路复用
|
||||
看到`select`,很自然的会联想到Linux提供的IO多路复用模型:`select`、`poll`、`epoll`,IO复用主要用于提升程序处理IO事件的性能。Go语言中的`select`与Linux中的`select`有一定的区别。操作系统中的IO多路复用简单理解就是用一个或者是少量线程处理多个IO事件。
|
||||
`select` 是 Go 原生提供的多路复用机制,用于在多个 channel 操作中选择一个执行。它是构建超时控制、心跳检测、任务调度等并发模式的核心原语。
|
||||
|
||||
### 传统阻塞IO vs IO多路复用
|
||||
## 正文
|
||||
|
||||
#### 传统阻塞IO
|
||||
对于每一个网络IO事件,操作系统都会起一个线程去处理,在IO事件没准备好的时候,当前线程就会一直阻塞。
|
||||
### select 是什么?
|
||||
|
||||

|
||||
> [!question] 💭 思考
|
||||
> 如果一个 goroutine 需要同时监听 3 个 channel,但一次只有一个有数据,怎么高效地处理而不阻塞?
|
||||
|
||||
优缺点:
|
||||
- 优点:逻辑简单,在阻塞等待期间线程会挂起,不会占用 CPU 资源
|
||||
- 缺点:每个连接需要独立的线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大
|
||||
`select` 的语法类似于 `switch`,但每个 case 必须是 channel 操作:
|
||||
|
||||
#### IO多路复用
|
||||
IO多路复用的基本原理如下图所示:
|
||||
|
||||

|
||||
|
||||
优缺点:
|
||||
- 优点:通过复用一个线程处理了多个IO事件,无需对额外过多的线程维护管理,资源和效率上都获得了提升
|
||||
- 缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低
|
||||
|
||||
Go语言的`select`语句,是用来起一个`goroutine`监听多个`Channel`的读写事件,提高从多个`Channel`获取信息的效率,相当于也是单线程处理多个IO事件,其思想基本相同。
|
||||
|
||||
## select用法
|
||||
`select`的基本使用模式如下:
|
||||
```go
|
||||
select {
|
||||
case <- channel1: // 如果从channel1读取数据成功,执行case语句
|
||||
do ...
|
||||
case channel2 <- 1: // 如果向channel2写入数据成功,执行case语句
|
||||
do ...
|
||||
default: // 如果上面都没有成功,进入default处理流程
|
||||
do ...
|
||||
case msg := <-ch1: // ch1 可读时执行
|
||||
fmt.Println("received", msg)
|
||||
case ch2 <- data: // ch2 可写时执行
|
||||
fmt.Println("sent")
|
||||
default: // 所有 case 都无法立即执行时
|
||||
fmt.Println("no channel ready")
|
||||
}
|
||||
```
|
||||
可以看到,`select`的用法形式类似于`switch`,但是区别于`switch`的是,`select`各个`case`的表达式必须都是`channel`的读写操作。`select`通过多个`case`语句监听多个`channel`的读写操作是否准备好可以执行,其中任何一个`case`可以执行了则选择该`case`语句执行,如果没有可以执行的`case`,则执行`default`语句,如果没有`default`,则当前`goroutine`会阻塞。
|
||||
|
||||
### 空select永久阻塞
|
||||
当一个`select`中什么语句都没有,没有任何`case`,将会永久阻塞:
|
||||
**核心行为规则:**
|
||||
1. 所有 case 的 channel 操作都会被求值
|
||||
2. 如果有多个 case 同时就绪,**随机选择一个**执行
|
||||
3. 没有 case 就绪且有 `default`,立即执行 default
|
||||
4. 没有 case 就绪且无 `default`,**阻塞等待**
|
||||
|
||||
> [!warning] ⚠️ 致命陷阱:空 select 导致死锁
|
||||
> ```go
|
||||
> func main() {
|
||||
> select {} // ❌ 没有任何 case,永久阻塞
|
||||
> }
|
||||
> ```
|
||||
> Go 运行时检测到所有 goroutine 都阻塞时会报 `deadlock` 错误。这是调试并发程序时最常见的 panic 之一。
|
||||
|
||||
### 超时控制模式
|
||||
|
||||
> [!tip] 💡 最实用的 select 模式:超时控制
|
||||
|
||||
```go
|
||||
package main
|
||||
// 方式1:time.After(简洁)
|
||||
select {
|
||||
case result := <-doWork():
|
||||
fmt.Println("完成:", result)
|
||||
case <-time.After(5 * time.Second):
|
||||
fmt.Println("超时!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 方式2:time.Timer(可取消,推荐)
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
defer timer.Stop() // 提前返回时释放资源
|
||||
|
||||
select {
|
||||
case result := <-doWork():
|
||||
fmt.Println("完成:", result)
|
||||
timer.Stop() // 完成后停止定时器
|
||||
case <-timer.C:
|
||||
fmt.Println("超时!")
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] 📝 Timer vs After
|
||||
> - `time.After(d)` 内部创建一个 Timer,无法提前释放——超时期间定时器仍在运行
|
||||
> - `time.NewTimer(d)` 可随时调用 `.Stop()` 释放底层资源,适合长生命周期场景
|
||||
|
||||
### 随机选择
|
||||
|
||||
当多个 case 同时就绪时,Go 会伪随机选择一个:
|
||||
|
||||
```go
|
||||
ch1 := make(chan int, 1)
|
||||
ch2 := make(chan int, 1)
|
||||
ch1 <- 1
|
||||
ch2 <- 2
|
||||
|
||||
select {
|
||||
case v := <-ch1:
|
||||
fmt.Println("ch1:", v) // 可能是 ch1 或 ch2
|
||||
case v := <-ch2:
|
||||
fmt.Println("ch2:", v) // 结果不确定
|
||||
}
|
||||
```
|
||||
|
||||
> [!info] ℹ️ 补充
|
||||
> 这种随机性是为了避免"饥饿"——总是优先选择同一个 channel 会导致其他 channel 永远得不到服务。如果你需要确定性顺序,应使用其他方式编排。
|
||||
|
||||
### 非阻塞检查
|
||||
|
||||
利用 `default` 实现非阻塞的 channel 读写:
|
||||
|
||||
```go
|
||||
select {
|
||||
case msg := <-ch:
|
||||
fmt.Println("收到消息:", msg)
|
||||
default:
|
||||
fmt.Println("当前没有待处理的消息") // 不会阻塞
|
||||
}
|
||||
```
|
||||
|
||||
这在轮询场景或实现高性能服务端中非常有用。
|
||||
|
||||
### 实际应用场景
|
||||
|
||||
#### 场景1:带超时的 RPC 调用
|
||||
|
||||
```go
|
||||
func callRPC(ctx context.Context, query string) (string, error) {
|
||||
resultCh := make(chan string, 1)
|
||||
|
||||
go func() {
|
||||
resultCh <- doRemoteCall(query)
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
return result, nil
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err() // 被取消
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
fatal error: all goroutines are asleep - deadlock!
|
||||
```
|
||||
程序因为`select`语句导致永久阻塞,当前`goroutine`阻塞之后,由于Go语言自带死锁检测机制,发现当前`goroutine`永远不会被唤醒,会报上述死锁错误。
|
||||
|
||||
### 没有default且case无法执行的select永久阻塞
|
||||
看下面示例:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch1 := make(chan int, 1)
|
||||
ch2 := make(chan int, 1)
|
||||
select {
|
||||
case <-ch1:
|
||||
fmt.Printf("received from ch1")
|
||||
case num := <-ch2:
|
||||
fmt.Printf("num is: %d", num)
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
fatal error: all goroutines are asleep - deadlock!
|
||||
```
|
||||
程序中 `select`从两个`channel`,`ch1`和`ch2`中读取数据,但是两个`channel`都没有数据,且没有`goroutine`往里面写数据,所以不可能读到数据,这两个`case`永远无法执行到,`select`也没有`default`,所以会出现永久阻塞,报死锁。
|
||||
|
||||
### 有单一case和default的select
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch := make(chan int, 1)
|
||||
select {
|
||||
case <-ch:
|
||||
fmt.Println("received from ch")
|
||||
default:
|
||||
fmt.Println("default!!!")
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
default!!!
|
||||
```
|
||||
执行到`select`语句的时候,由于`ch`中没有数据,且没有`goroutine`往`channel`中写数据,所以`case`不可能执行到,就会执行`default`语句,打印出`default!!!`。
|
||||
|
||||
### 有多个case和default的select
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch1 := make(chan int, 1)
|
||||
ch2 := make(chan int, 1)
|
||||
go func() {
|
||||
time.Sleep(time.Second)
|
||||
for i := 0; i < 3; i++ {
|
||||
select {
|
||||
case v := <-ch1:
|
||||
fmt.Printf("Received from ch1, val = %d\n", v)
|
||||
case v := <-ch2:
|
||||
fmt.Printf("Received from ch2, val = %d\n", v)
|
||||
default:
|
||||
fmt.Println("default!!!")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
ch1 <- 1
|
||||
time.Sleep(time.Second)
|
||||
ch2 <- 2
|
||||
time.Sleep(4 * time.Second)
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
Received from ch1, val = 1
|
||||
Received from ch2, val = 2
|
||||
default!!!
|
||||
```
|
||||
主`goroutine`中向后往管道`ch1`和`ch2`中发送数据,在子`goroutine`中执行两个`select`,可以看到,在执行`select`的时候,那个`case`准备好了就会执行当下`case`的语句,最后没有数据可接受了,没有`case`可以执行,则执行`default`语句。
|
||||
|
||||
> 注意:当多个case都准备好了的时候,会随机选择一个执行
|
||||
#### 场景2:优雅关闭
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch1 := make(chan int, 1)
|
||||
ch2 := make(chan int, 1)
|
||||
ch1 <- 5
|
||||
ch2 <- 6
|
||||
select {
|
||||
case v := <-ch1:
|
||||
fmt.Printf("Received from ch1, val = %d\n", v)
|
||||
case v := <-ch2:
|
||||
fmt.Printf("Received from ch2, val = %d\n", v)
|
||||
default:
|
||||
fmt.Println("default!!!")
|
||||
func monitor(ctx context.Context, ticker <-chan time.Time) {
|
||||
for {
|
||||
select {
|
||||
case t := <-ticker:
|
||||
fmt.Println("heartbeat at", t)
|
||||
case <-ctx.Done():
|
||||
fmt.Println("shutting down...")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
Received from ch2, val = 6
|
||||
```
|
||||
多次执行,2个`case`都有可能打印,这就是`select`选择的随机性。
|
||||
|
||||
> [!warning] ⚠️ 并发安全提醒
|
||||
> - select 本身是并发安全的,多个 goroutine 可以同时执行不同的 select
|
||||
> - 但不要在一个 goroutine 中对同一 channel 既读又写(除非你清楚自己在做什么)
|
||||
> - 永远记得:关闭未初始化的 nil channel 和重复关闭都会 panic
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[hzh/GolangStar/Go语言进阶/Channel]]
|
||||
- [[hzh/GolangStar/Go语言进阶/Context]]
|
||||
- [[hzh/GolangStar/Go语言进阶/定时器]]
|
||||
|
||||
Reference in New Issue
Block a user