Go 语言 select 的实现原理解析

 更新时间:2025年01月19日 09:46:05   作者:不7夜宵  
select是Go在语言层面提供的I/O多路复用的机制,其专门用来让Goroutine同时等待多个channel是否准备完毕:可读或可写,这篇文章主要介绍了Go 语言 select 的实现原理,需要的朋友可以参考下

介绍

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
}

 性能优化建议

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

到此这篇关于Go 语言 select 的实现原理的文章就介绍到这了,更多相关Go 语言 select内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go结构体从基础到应用深度探索

    Go结构体从基础到应用深度探索

    本文深入探讨了结构体的定义、类型、字面量表示和使用方法,旨在为读者呈现Go结构体的全面视角,通过结构体,开发者可以实现更加模块化、高效的代码设计,这篇文章旨在为您提供关于结构体的深入理解,助您更好地利用Go语言的强大功能
    2023-10-10
  • Golang内存泄露场景与定位方式的实现

    Golang内存泄露场景与定位方式的实现

    Golang有自动垃圾回收机制,但是仍然可能会出现内存泄漏的情况,本文主要介绍了Golang内存泄露场景与定位方式的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-04-04
  • 一文详解Golang的中间件设计模式

    一文详解Golang的中间件设计模式

    最近在看一些rpc框架的使用原理和源码的时候,对中间件的实现非常感兴趣,所以这篇文章就来和大家聊聊Golang的中间件设计模式,希望对大家有所帮助
    2023-03-03
  • Go语言基础之网络编程全面教程示例

    Go语言基础之网络编程全面教程示例

    这篇文章主要为大家介绍了Go语言基础之网络编程全面教程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • 利用GO语言实现多人聊天室实例教程

    利用GO语言实现多人聊天室实例教程

    聊天室的实现大家应该都遇到过,这篇文章主要给大家介绍了关于利用GO语言实现多人聊天室的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2018-03-03
  • Go语言实现选择法排序实例

    Go语言实现选择法排序实例

    这篇文章主要介绍了Go语言实现选择法排序的方法,实例分析了选择法排序的实现技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-02-02
  • Go中最强大的权限控制库(Casbin)的实现

    Go中最强大的权限控制库(Casbin)的实现

    Casbin是一个强大的、高效的开源访问控制框架,支持ACL、RBAC、ABAC 等多种经典访问控制模型,通过配置文件即可灵活定义权限规则,具有一定的参考价值,感兴趣的可以了解一下
    2026-03-03
  • go语言之go(goroutine)控制异步详解

    go语言之go(goroutine)控制异步详解

    Go语言通过goroutine实现并发,允许异步执行函数,但单独使用会引发顺序问题,需结合WaitGroup确保主线程等待所有并发任务完成后再输出结果,从而正确同步执行流程
    2025-07-07
  • go语言单例模式(Singleton)实例分析

    go语言单例模式(Singleton)实例分析

    这篇文章主要介绍了go语言单例模式(Singleton),实例分析了单例模式的原理与Go语言的实现技巧,需要的朋友可以参考下
    2015-03-03
  • golang中使用匿名结构体的方法

    golang中使用匿名结构体的方法

    这篇文章主要介绍了golang中使用匿名结构体,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-08-08

最新评论