golang select 的使用及基本实现

Mastering Go’s Select: Beyond the Basics

golang select (from github.com/MariaLetta/free-gophers-pack)

1.1 select 简介:

在 Go 语言中,select 语句用于处理多个通道操作,从而简化并发编程中的通信和同步问题。select 语句类似于 switch 语句,但它的每个 case 都必须是一个通道操作。

以下是 select 语句的基本语法:

1
2
3
4
5
6
7
8
select {
case <-chan1:
// Executed when chan1 has data to receive
case chan2 <- value:
// Executed when data can be sent to chan2
default:
// Executed when none of the cases are satisfied
}

使用场景

  • 多通道选择:可以同时等待多个通道操作,当其中任何一个通道准备好时,程序就会执行相应的 case。
  • 超时处理:通过结合 time.After 函数,可以实现超时机制。
  • 非阻塞通信:通过使用 default 子句,可以实现非阻塞的通道操作。

关于超时处理,可以参考文章:Time Control You Should Know in Golang | by Wesley Wei | Programmer’s Career

1.2 select 与通道:

与 switch 不同的是,select 中虽然也有多个 case,但是这些 case 中的表达式都必须与 Channel 的操作有关,也就是 Channel 的读写操作。 它可以帮助开发者避免因为等待某个阻塞的 Channel导致程序死锁。

示例 1:从多个通道接收数据

以下是一个例子,展示了如何使用 select 语句从两个通道中接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"time"
)

func main() {
chan1 := make(chan string)
chan2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
chan1 <- "Message from channel 1"
}()

go func() {
time.Sleep(2 * time.Second)
chan2 <- "Message from channel 2"
}()

select {
case msg1 := <-chan1:
fmt.Println(msg1)
case msg2 := <-chan2:
fmt.Println(msg2)
}
}

在这个示例中,我们创建了两个通道 chan1 和 chan2,并通过两个协程分别在 1 秒和 2 秒后向这两个不同的通道发送消息。select 语句会等待第一个通道准备好并接收消息,然后输出相应的信息。

示例 2:处理超时情况

以下是一个例子,展示了如何使用 select 语句与超时机制结合来处理多个通道操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"time"
)

func main() {
chan1 := make(chan string)
chan2 := make(chan string)

go func() {
time.Sleep(3 * time.Second)
chan1 <- "Message from channel 1"
}()

go func() {
time.Sleep(2 * time.Second)
chan2 <- "Message from channel 2"
}()

select {
case msg1 := <-chan1:
fmt.Println(msg1)
case msg2 := <-chan2:
fmt.Println(msg2)
case <-time.After(1 * time.Second):
fmt.Println("Timeout, no channel was ready within 1 second")
}
}

在这个示例中,select 语句会等待读取 chan1 和 chan2 中的数据。如果超过了 1 秒钟还没有通道准备好,select 语句会执行第三个 case,打印 “Timeout, no channel was ready within 1 second”。

示例 3:非阻塞通信

最后,我们来看一个示例,展示了如何使用 default 子句来实现非阻塞通道操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)

func main() {
chan1 := make(chan string)
select {
case msg := <-chan1:
fmt.Println(msg)
default:
fmt.Println("Default case: Channel is not ready")
}
}

在这个示例中,如果 chan1 没有数据可接收,select 语句会执行 default 子句,打印 “Default case: Channel is not ready”。

1.3 select 的实现:

基于Go 1.23 分析

1.3.1 数据结构

select 在 Go 语言的源代码中不存在对应的结构体,但是我们使用 runtime.scase 结构体表示 select 控制结构中的 case

1
2
3
4
type scase struct {
c *hchan // chan
elem unsafe.Pointer // data element
}

因为非默认的 case 中都与 Channel 的发送和接收有关,所以 runtime.scase 结构体中也包含一个 runtime.hchan 类型的字段存储 case 中使用的 Channel。

1.3.2 常见流程

在默认的情况下,编译器会使用如下的流程处理 select 语句:

  1. 将所有的 case 转换成包含 Channel 以及类型等信息的 runtime.scase 结构体;
  2. 调用运行时函数 runtime.selectgo 从多个准备就绪的 Channel 中选择一个可执行的 runtime.scase 结构体;
  3. 通过 for 循环生成一组 if 语句,在语句中判断自己是不是被选中的 case

1.3.3 随机轮训、加锁顺序

runtime.selectgo 函数首先会进行执行必要的初始化操作并决定处理 case 的两个顺序 — 轮询顺序 pollOrder 和加锁顺序 lockOrder

轮询顺序 pollOrder 和加锁顺序 lockOrder 分别是通过以下的方式确认的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
// Casts the `cas0` pointer to an array of scase (channel cases)
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
// Casts the `order0` pointer to an array of uint16 values for polling order
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
...
// Calculates the total number of cases (sending + receiving channels)
ncases := nsends + nrecvs
// Creates slices for the cases and polling/locking orders
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]

norder := 0
// Iterates through each channel case
for i := range scases {
cas := &scases[i] // Retrieves a pointer to the current scase (channel case)
...
j := cheaprandn(uint32(norder + 1)) // Generates a random number up to i
pollorder[norder] = pollorder[j] // Swaps current poll order with a random one
pollorder[j] = uint16(i)
norder++
}
// Trims the pollorder and lockorder slices to the number of cases
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]

// Sorts the lock order based on channel addresses to avoid deadlocks
...
sellock(scases, lockorder) // Locks the channels in the determined order
...
}

func sellock(scases []scase, lockorder []uint16) {
var c *hchan
for _, o := range lockorder {
c0 := scases[o].c
if c0 != c {
c = c0
lock(&c.lock)
}
}
}
  • 轮询顺序:通过 runtime.cheaprandn 函数引入随机性;
  • 加锁顺序:按照 Channel 的地址排序后确定加锁顺序;

随机的轮询顺序可以避免 Channel 的饥饿问题,保证公平性;而根据 Channel 的地址顺序确定加锁顺序能够避免死锁的发生。这段代码最后调用的 runtime.sellock 会按照之前生成的加锁顺序锁定 select 语句中包含所有的 Channel。

1.4 性能考量:

  1. 注册和选择的开销

当一个 select 语句被执行时,Go 运行时需要将每个通道操作注册到调度队列上,并在通道可用时从队列中解除阻塞。对于每个 select 语句,注册和选择的操作带来了额外的调度开销。

  1. 随机选择的开销

当有多个通道同时准备好时,Go 运行时会随机选择一个通道执行。这种随机选择涉及随机数生成和对所有就绪通道的扫描,这一过程带来了额外的开销。

  1. 阻塞和唤醒的开销

select 阻塞等待通道时,Go 运行时需要将当前的 goroutine 放入等待队列,等待通道有数据可处理。阻塞和唤醒的过程涉及 goroutine 的调度,这也会引入一定的上下文切换开销。

1.5 I/O 多路复用:

经过上面的分析,相信你会有熟悉的感觉;没错,我也想到了I/O 多路复用。在操作系统中select 是系统调用,我们经常会使用 selectpoll 和 epoll 等函数构建 I/O 多路复用模型提升程序的性能。

Go 语言的 select 与操作系统中的 select 比较相似,比如 C 语言的 select 系统调用可以同时监听多个文件描述符的可读或者可写的状态,而 Go 语言中的 select 也能够让 Goroutine 同时等待多个 Channel 可读或者可写。

他们的抽象形态类似下图:

IO多路复用

1.6 总结

表面上来看:

Go 的select语句是一种仅能用于channel发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中没有case语句的时候,会阻塞当前的goroutine。所以,select是用来阻塞监听goroutine的。

深层次来理解的话:
select是 Go 在语言层面提供的I/O多路复用的机制,其专门用来检测多个可读或可写的 Channel 是否准备完毕。

1.7 参考

Go 语言 select 的实现原理 | Go 语言设计与实现

更多该系列文章,参考medium链接:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/golang-select/
作者:Wesley Wei – Twitter Wesley Wei – Medium
发表日期:原文在 2024-09-21 16:32 时创作于 https://programmerscareer.com/zh-cn/golang-select/
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

Golang Sync.Once 使用与原理,看这一篇足矣 golang中你应该知道的defer知识(基础应用篇)

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×