当前位置: 首页 > news >正文

Go select 语句使用场景

1. select介绍

select 是 Go 语言中的一种控制结构,用于在多个通信操作中选择一个可执行的操作。它可以协调多个 channel 的读写操作,使得我们能够在多个 channel 中进行非阻塞的数据传输、同步和控制。

基本语法:

select {case communication clause  :statement(s);case communication clause  :statement(s);/* 你可以定义任意数量的 case */default : /* 可选 */statement(s);
}

如果多个 case 都可以运行,select 会随机公平地选出一个执行。如果没有 case 可以运行,它要么阻塞(等待 case),要么执行default子句。

2. select 语句的常用使用场景:

  • 等待多个通道的消息(多路复用)

    当我们需要等待多个通道的消息时,使用 select 语句可以非常方便地等待这些通道中的任意一个通道有消息到达,从而避免了使用多个goroutine进行同步和等待。

  • 超时等待通道消息

    当我们需要在一段时间内等待某个通道有消息到达时,使用 select 语句可以与 time 包结合使用实现定时等待。

  • 在通道上进行非阻塞读写

    在使用通道进行读写时,如果通道没有数据,读操作或写操作将会阻塞。但是使用 select 语句结合 default 分支可以实现非阻塞读写,从而避免了死锁或死循环等问题。

因此,select 的主要作用是在处理多个通道时提供了一种高效且易于使用的机制,简化了多个 goroutine 的同步和等待,使程序更加可读、高效和可靠。

3. 代码示例:

代码1:

package mainimport ("fmt""time"
)func main() {chan1 := make(chan int)chan2 := make(chan int)go func() {chan1 <- 1time.Sleep(5 * time.Second)}()go func() {chan2 <- 1time.Sleep(5 * time.Second)}()select {case <-chan1:fmt.Println("chan1")case <-chan2:fmt.Println("chan2")default:fmt.Println("default")}fmt.Println("main exit")
}

输出结果为:

可能会出现三种结果:

chan1
main exit
chan2
main exit
default
main exit

select 中的 case 执行顺序是随机的,如果某个 case 中的 channel 已经 ready,那么就会执行相应的语句并退 出 select 流程,如果所有 case 中的 channel 都未 ready,那么就会执行 default 中的语句然后退出 select 流程。

由于启动的协程和 select 语句并不能保证执行的顺序,所以也有可能 select 执行时协程还未向channel中写入数据,所以 select 直接执行 default 语句并退出。因此,程序有可能产生三种输出

代码2:

package mainimport ("fmt"
)func main() {chan1 := make(chan int)chan2 := make(chan int)go func() {close(chan1)}()go func() {close(chan2)}()select {case <- chan1:fmt.Println("chan1")case <- chan2:fmt.Println("chan2")}fmt.Println("main exit.")
}

select 会随机检测各 case 语句中 channel 是否 ready,注意已关闭的 channel 也是可读的,所以上述程序中select 不会阻塞,具体执行哪个 case 语句是随机的。

代码3:

package mainfunc main() {select {}
}

对于空的 select 语句,程序会被阻塞,确切的说是当前协程被阻塞,同时 Go 自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则会发生 panic。所以上述程序会 panic。

定时器实现定时任务的执行代码:

package mainimport ("fmt""time"
)func main() {fmt.Println("定时任务开始")// 创建一个每秒触发一次的定时器ticker := time.NewTicker(1 * time.Second)done := make(chan bool)go func() {for {select {case <-ticker.C:fmt.Println("执行定时任务")case <-done:ticker.Stop()return}}}()// 等待5秒time.Sleep(5 * time.Second)done <- truefmt.Println("定时任务结束")
}

结果:

定时任务开始
执行定时任务
执行定时任务
执行定时任务
执行定时任务
执行定时任务
定时任务结束

超时退出实现代码:

package mainimport ("fmt""time"
)func main() {timeout := 5 * time.Seconddone := make(chan bool)go func() {// 模拟耗时操作time.Sleep(2 * time.Second)done <- true}()select {case <-done:fmt.Println("Task completed successfully.")case <-time.After(timeout):fmt.Println("Timeout! The operation took too long.")}
}

或:

package mainimport ("context""fmt""time"
)func main() {timeout := 5 * time.Secondctx, cancel := context.WithTimeout(context.Background(), timeout)defer cancel()done := make(chan bool)go func() {// 模拟耗时操作time.Sleep(2 * time.Second)done <- true}()select {case <-done:fmt.Println("Task completed successfully.")case <-ctx.Done():fmt.Println("Timeout! The operation took too long.")}
}

4. 总结

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

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

  • select 语句中如果没有 default 语句,则会阻塞等待任一 case

  • select 语句中读操作要判断是否成功读取,关闭的 channel 也可以读取

select在 Go 语言的源代码中不存在对应的结构体,只是定义了一个 runtime.scase 结构体(在src/runtime/select.go)表示每个 case 语句(包含defaut):

// Select case descriptor.
// Known to compiler.
// Changes here must also be made in src/cmd/compile/internal/walk/select.go's scasetype.
type scase struct {c    *hchan         // chanelem unsafe.Pointer // data element
}

因为所有的非 default 的 case 基本都要求是对Channel的读写操作,所以 runtime.scase 结构体中也包含一个 runtime.hchan 类型的字段存储 case 中使用的 Channel,另一个字段 elem 指向 case 条件包含的数据的指针,如 case ch1 <- 1,则 elem 指向常量1.

编译器会对select有不同的case的情况进行优化以提高性能。首先,编译器对select没有case、有单case和单case+default的情况进行单独处理,这些处理或者直接调用运行时函数,或者直接转成对channel的操作,或者以非阻塞的方式访问channel,多种灵活的处理方式能够提高性能,尤其是避免对channel的加锁。

对最常出现的select有多case的情况,会调用runtime.selectgo()函数来获取执行case的索引

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)

selectgo函数内部逻辑:

  1. 使用fastrandn算法把scases数组的索引重新编排顺序。

  2. 根据新的索引顺序对hchan进行堆排序来获取case的锁定顺序。(保证 n log n 时间和恒定的堆栈占用空间)

  3. 锁定所有channel。

  4. 遍历所有channel,判断是否有可读或者可写的,如果有,解锁channel,返回对应数据。

  5. 否则,判断有没有default,如果有,解锁channel,返回default对应scase。

  6. 否则,把当前groutian添加到所有channel的等待队列里,解锁所有channel,等待被唤醒。

  7. 被唤醒后,再次锁定所有channel

  8. 遍历所有channel,把g从channel等待队列中移除,并找到可操作的channel

  9. 如果对应的scase不为空,直接返回对应的值

  10. 否则循环此过程

相关文章:

  • 2024.06.08【读书笔记】丨生物信息学与功能基因组学(第十二章 全基因组和系统发育树 第四部分)【AI测试版】
  • Proxyman 现代直观的 HTTP 调试代理应用程序
  • 基于Texture2D 实现Unity 截屏功能
  • Elasticsearch 认证模拟题 - 13
  • 移动端投屏到大屏幕的操作详解
  • [office] 如何在Excel中拉动单元格时表头不变形- #学习方法#职场发展#经验分享
  • 使用 Ollama 和 Open WebUI 自托管 LLM 聊天机器人(无需 GPU)
  • Mongodb中字段的删除
  • Java面试八股之什么是自动装箱和自动拆箱
  • mac虚拟光驱工具:Daemon Tools for Mac
  • 2024 vite 静态 scp2 自动化部署
  • k8s网络问题以及容器跨宿主机通信原理
  • 代码随想录算法训练营第四天|24. 两两交换链表中的节点 |19.删除链表的倒数第N个节点 | 面试题 02.07. 链表相交 | 142.环形链表II
  • 【python】OpenCV GUI——Mouse(14.1)
  • 17、matlab实现均值滤波、中值滤波、Butterworth滤波和线性相位FIR滤波
  • [case10]使用RSQL实现端到端的动态查询
  • 0x05 Python数据分析,Anaconda八斩刀
  • ABAP的include关键字,Java的import, C的include和C4C ABSL 的import比较
  • Android路由框架AnnoRouter:使用Java接口来定义路由跳转
  • HTTP那些事
  • linux学习笔记
  • REST架构的思考
  • SSH 免密登录
  • storm drpc实例
  • vue总结
  • 蓝海存储开关机注意事项总结
  • 如何借助 NoSQL 提高 JPA 应用性能
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 使用 5W1H 写出高可读的 Git Commit Message
  • 小程序 setData 学问多
  • 白色的风信子
  • #NOIP 2014# day.2 T2 寻找道路
  • #如何使用 Qt 5.6 在 Android 上启用 NFC
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (Redis使用系列) Springboot 实现Redis 同数据源动态切换db 八
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (理论篇)httpmoudle和httphandler一览
  • (一)使用Mybatis实现在student数据库中插入一个学生信息
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • . Flume面试题
  • .mp4格式的视频为何不能通过video标签在chrome浏览器中播放?
  • .NetCore实践篇:分布式监控Zipkin持久化之殇
  • .NET精简框架的“无法找到资源程序集”异常释疑
  • @font-face 用字体画图标
  • [000-002-01].数据库调优相关学习
  • [2021 蓝帽杯] One Pointer PHP
  • [AIGC] Redis基础命令集详细介绍
  • [C/C++]关于C++11中的std::move和std::forward
  • [C++] Windows中字符串函数的种类
  • [C++] 容器适配器:深入理解Stack与Queue的底层原理
  • [C++]AVL树怎么转
  • [C++]运行时,如何确保一个对象是只读的
  • [CareerCup] 6.1 Find Heavy Bottle 寻找重瓶子
  • [Fri 26 Jun 2015 ~ Thu 2 Jul 2015] Deep Learning in arxiv