Go 语言 select 的实现原理

ops/2025/1/20 18:04:06/

介绍

select是Go在语言层面提供的I/O多路复用的机制,其专门用来让Goroutine同时等待多个channel是否准备完毕:可读或可写。在Channel状态改变之前,select会一直阻塞当前线程或者goroutine。

特性:

case 必须是一个通信操作,主要是指对通道(Channel)进行发送或者接收数据的操作。

select 语句中除 default 外,各 case 执行顺序是随机的。

select 语句中如果没有 default 语句,则会阻塞等待任意一个 case满足执行条件。

select 语句中除 default 外,每个 case 只能操作一个 channel,要么读要么写。

当 select 中的多个 case 同时被触发时,会随机执行其中的一个。

普通多线程

多路复用

 数据结构

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

type scase struct {c    *hchan                    //case操作的通道     kind  uint16//表示该case的类型,分为读channel、写channel和default。//读channel、写channel和default三种类型分别由常量定义//caseRecv:case语句中尝试读取scase.c中的数据。//caseSend:case语句中尝试向scase.c中写入数据。//caseDefault:default语句。elem unsafe.Pointer //scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址;//scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;
}

在select语句运行时,scase结构体的实例会被用来表示每个case。运行时根据c 字段找到对应的通道,根据elem字段来处理数据的发送或接收操作。

执行流程

 实现过程和结果(穿插编译器的重写和优化)

单分支的select

只有一个 case 且不是 default,这种情况编译器会直接将其翻译成对管道的收发操作,并且还是阻塞式的,一直阻塞到操作可以完成。

对于接收操作(如 case val := <-ch),它会被转换为 val := <-ch,直接尝试从通道 ch 接收数据。

对于发送操作(如 case ch <- value),它会被转换为 ch <- value,直接尝试向通道 ch 发送数据。

只包含default分支会直接执行default操作。

多路select

在编译器中会被转换为runtime.selectgo函数调用。

func selectgo(cas0 *scase, order0 *uint16, , ncases int) (int, bool) {pollorder := order1[:ncases:ncases]lockorder := order1[ncases:][:ncases:ncases]for i := 1; i < ncases; i++ {j := fastrandn(uint32(i + 1))pollorder[i] = pollorder[j]pollorder[j] = uint16(i)}// 代码可能继续执行后续操作
}
  • cas0,scase数组的头部指针,前半部分存放的是写管道 case,后半部分存放的读管道 case,以nsends来区分

  • order0,它的长度是scase数组的两倍,前半部分分配给pollorder数组(决定管道执行顺序),后半部分分配给lockorder数组(决定管道锁定顺序)

  • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。

  • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。

  • ncases表示scase数组的长度

直接阻塞

1. select结构不包含任何case

在Go编译器内部的代码如下

func walkselectcases(cases *Nodes) []*Node {n := cases.Len()if n == 0 {return []*Node{mkcall("block", nil, nil)}}...
}func block() {gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}

walkselectcases的参数是一个select语句中的case元素的集合。当集合的长度为0时,表示当前select中无case会调用block函数,block函数会调用gopark让出goroutine对处理器的使用权并传入等待原因,暂停goroutine避免CPU空转。

2.当case中的channel是空指针

例:包含一个case且case中的channel是空指针,编译器会将select改写为if条件语句

//部分代码
if ch == nil {block()调用block,将goroutine陷入永久休眠
}

非阻塞操作 

当select中包含default分支时,就会被编译器认为是一次非阻塞的收发操作。

示例:一个case分支一个default分支,对通道的读写操作

示例:一个case分支一个default分支,对通道的读写操作

写操作:编译器会使用条件语句和 runtime.selectnbsend 函数改写代码

//改写
if selectnbsend(ch, i) {...
} else {...
}
//false参数决定了这一次的发送是非阻塞的,所以如果存在缓冲区空间不足时,当前 Goroutine 都不会阻塞而是会直接返回。
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {return chansend(c, elem, false, getcallerpc())
}

 读操作:

// 改写前
select {
case v <- ch: // case v, ok <- ch:......
default:......
}// 改写后
if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &ok, ch) {...
} else {...
}
//看读操作是否需要,第一个会忽略返回的布尔值,第二个会将布尔值传给调用方,block参数决定本次操作不阻塞
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {selected, _ = chanrecv(c, elem, false)return
}func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {selected, *received = chanrecv(c, elem, false)return
}

 性能优化建议

  1. case数量控制建议不超过5-10个
  2. 适当使用带缓冲区的channel避免频繁的阻塞和唤醒
  3. 合理使用default避免无谓的阻塞


http://www.ppmy.cn/ops/151729.html

相关文章

K8S的探针说明和使用方式

探针概述 探针分类 K8S中 探针&#xff08;Probes&#xff09; 是用于检查容器的健康状况和可用性的机制。探针可以自动判断应用的运行状态&#xff0c;并根据需要重启容器、替换容器或将流量路由到健康的实例。从而确保应用始终处于健康、可用的状态&#xff0c;并帮助自动化…

Rust 错误处理(下)

目录 1、用 Result 处理可恢复的错误 1.1?传播错误的简写&#xff1a;? 运算符 1.2 哪里可以使用 ? 运算符 2、要不要 panic! 2.1?示例、代码原型和测试都非常适合 panic 2.2?当我们比编译器知道更多的情况 2.3?错误处理指导原则 2.4?创建自定义类型进行有效性验…

MySQL表的创建实验

创建并使用数据库mydb6_product 。 mysql> create database mydb6_product; Query OK, 1 row affected (0.01 sec)mysql> use mydb6_product; Database changed 新建employees表。 对于gender&#xff0c;有默认值意味着不为空&#xff0c;在建表时可以选择不写not nul…

会话_JSP_过滤器_监听器_Ajax

第8章 会话_JSP_过滤器_监听器_Ajax 8.1 会话 8.1.1 会话管理概述 1、为什么需要会话管理 HTTP是无状态协议&#xff1a; 无状态就是不保存状态&#xff0c;即无状态协议(stateless)&#xff0c;HTTP协议自身不对请求和响应之间的通信状态进行保存&#xff0c;也就是说&…

Centos 8 交换空间管理

新增swap 要增加 Linux 系统的交换空间&#xff0c;可以按照以下步骤操作&#xff1a; 1. 创建一个交换文件 首先&#xff0c;选择文件路径和大小&#xff08;例如&#xff0c;增加 1 GB 交换空间&#xff09;。 sudo fallocate -l 1G /swapfile如果 fallocate 不可用&…

斯坦福iDP3的Learning代码解析:逐步分解人形策略iDP3的数据集、模型、策略代码

前言 今25年1.14日起&#xff0c;我和同事孙老师连续出差苏州、无锡、南京、上海 1.14日在苏州&#xff0c;一家探讨人形合作研发&#xff0c;一家是客户1.15-1.16两天在南京&#xff0c;和同事姚博士、合作商一块接待一机器人集团客户 客户表示高校偏科研&#xff0c;但我们…

处理没有提示的字符串、计算相隔天数应用题

正常情况下&#xff0c;小云每天跑 1 千米。如果某天是周一或者月初&#xff08;1 日&#xff09;&#xff0c;为了激励自己&#xff0c;小云要跑 2 千米。如果同时是周一或月初&#xff0c;小云也是跑 2 千米。 小云跑步已经坚持了很长时间&#xff0c;从 1990 年 1 月 1 日周…

Ncat: bind to :::7777: Address already in use报错问题解决

问题描述 Ncat: bind to :::7777: Address already in use. QUITTING. 具体解决方法 If you are in linux environment try, Use netstat -tulpn to display the processeskill -9 <pid> This will terminate the process If you are using windows, Use netstat -…