5.3 select的底层实现及工作原理
select
语句是Go
语言中用于处理多个通道操作的一个强大工具,它能够在多个通道上同时进行非阻塞的选择操作。这对于实现并发程序的灵活性和复杂性处理非常有帮助。
本节我们将详细探讨select
的内部实现及工作原理。
本节代码存放目录为 lesson15
select的底层实现
select的基本结构
select
语句的实现涉及到以下几个核心部分:
-
通道的
case
列表:select
语句中每个case
会对应一个通道操作,编译器会将这些case
打包成一个select
操作列表。case
结构如下所示:type scase struct {c *hchan // chanelem unsafe.Pointer // data element }
-
随机化:为了避免
select
语句的饥饿问题(总是先处理某个case
),Go
语言的实现会对case
列表进行随机化处理。 -
阻塞队列:如果所有的通道都无法立即进行操作,
select
语句会将当前的Goroutine
加入到每个通道的等待队列中,并阻塞Goroutine
,直到某个通道的操作可以进行。 -
唤醒与继续:当某个通道的操作可以进行时,
select
会唤醒相关的Goroutine
,并继续执行与该通道关联的case
。
select的操作流程
我们可以以下步骤理解select
的操作流程:
-
初始化
select
的case
列表:编译器将每个case
操作(通道的接收或发送)打包到一个列表中。 -
随机化
case
列表:为了避免饥饿,运行时会对这个case
列表进行随机打乱,使得每次select
的执行顺序都是随机的。 -
遍历
case
列表:-
对于每个
case
,select
语句会检查对应通道是否可以立即进行操作。 -
如果可以,则直接执行该
case
,并结束select
语句。 -
如果不可以,则将当前
Goroutine
加入到该通道的等待队列中。
-
-
阻塞当前
Goroutine
:-
如果所有的通道都不能立即操作,
select
语句将阻塞当前的Goroutine
,直到其中一个通道可以进行操作。 -
当某个通道准备好后,该
Goroutine
会被唤醒,执行与该通道关联的case
。
-
-
默认情况
default
:- 如果
select
语句中存在default
分支,并且所有通道都不能操作,那么select
会立即执行default
分支,而不会阻塞。
- 如果
我们可以通过下面的示意图来进行理解:
┌────────────────────────┐
│ select │
│ ┌───────────────────┐ │
│ │ case1: <- ch1 │ │
│ │ case2: <- ch2 │ │
│ │ case3: <- ch3 │ │
│ └───────────────────┘ │
└────────────────────────┘│▼
┌─────────────────────────┐
│ 运行时随机化 │
│ 随机打乱 case 列表 │
└─────────────────────────┘│▼
┌─────────────────────────┐
│ 顺序检查 case │
│ 检查 case1、case2... │
│ 按随机后的顺序 │
└─────────────────────────┘│▼
┌─────────────────────────┐
│ 执行一个可以操作的 case │
│ 例如:执行 case2 │
└─────────────────────────┘│▼select 语句结束
select的实现原理
Go
语言中的select
语句依赖于调度器和通道的底层机制来实现。具体来说:
-
调度器:
select
语句会与Go
调度器紧密合作,当select
阻塞时,调度器会将当前Goroutine
挂起,并将其加入到通道的等待队列中。 -
通道的队列:每个通道都有发送和接收的等待队列。当
select
中的某个通道准备好时,通道的机制会从队列中唤醒对应的Goroutine
。 -
唤醒机制:当通道的状态发生变化时(例如一个通道的数据被接收或发送),通道会通过调度器唤醒阻塞在其上的
Goroutine
,然后继续执行select
语句的逻辑。
性能与使用建议
虽然select
非常强大,但是在使用时也有一些性能和设计方面的考虑:
-
避免滥用
select
:在高并发场景下,如果select
语句处理的通道数量过多,可能会带来一些性能开销。 -
使用
default
分支:在某些情况下,添加default
分支可以防止select
语句永久阻塞,从而提高程序的响应性。 -
关注
select
的随机性:由于select
语句的case
选择是随机化的,因此不要依赖某个固定的选择顺序,这样可以避免一些难以调试的问题。
下面代码演示了一个常用的使用案例:
func main() {ch1 := make(chan int64, 2)ch2 := make(chan int64, 2)ch3 := make(chan int64, 2)wg.Add(1)go func() {defer wg.Done()for {ch1 <- time.Now().Unix()time.Sleep(time.Duration(1) * time.Second)ch2 <- time.Now().Unix()time.Sleep(time.Duration(1) * time.Second)ch3 <- time.Now().Unix()time.Sleep(time.Duration(1) * time.Second)}}()wg.Add(1)go func() {defer wg.Done()for {select {case t1 := <-ch1:fmt.Println("Received from ch1, ", t1)case t2 := <-ch2:fmt.Println("Received from ch2, ", t2)case t3 := <-ch3:fmt.Println("Received from ch3, ", t3)}}}()wg.Wait()
}
小结
select
的主要作用就是用于对多个通道执行读取操作,这样一方面我们可以简化我们的程序,一方面我们也可以通过select
执行一些流程操作。
select
本质上就属于是监听了多个通道,所以我们不适合在select
中使用大批量的case
。
我的GitHub:https://github.com/swxctx
书籍地址:https://d.golang.website/
书籍代码:https://github.com/YouCanGolang/GoDeeperCode