首页 > 其他分享 >go语言select

go语言select

时间:2023-05-31 20:36:08浏览次数:44  
标签:case 语言 channel selectgo select go runtime Channel

go语言select

使用

func main() {
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 1)

	go func() {
		time.Sleep(1 * time.Second)
		ch1 <- 1
	}()

	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 1
	}()

	select {
	case <-ch1:
		fmt.Println("ch 1 out")
	case <-ch2:
		fmt.Println("ch 2 out")
	default:
		fmt.Println("default")
	}

}

C 语言的 select 系统调用可以同时监听多个文件描述符的可读或者可写的状态,Go 语言中的 select 也能够让 Goroutine 同时等待多个 Channel 可读或者可写,在多个文件或者 Channel状态改变之前,select 会一直阻塞当前线程或者 Goroutine。

当我们在 Go 语言中使用 select 控制结构时,会遇到两个有趣的现象:

  1. select 能在 Channel 上进行非阻塞的收发操作;
  2. select 在遇到多个 Channel 同时响应时,随机的引入就是为了避免饥饿问题的发生;

数据结构

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

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

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

实现原理

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

  1. 将所有的case转换成包含channel以及类型等信息的runtime.scase结构体
  2. 调用运行时函数runtime.selectgo从多个准备就绪的channel中选择一个可执行的runtime.scase结构体
  3. 通过for循环生成一组if语句,在语句中判断自己是不是被选中的case
selv := [3]scase{}
order := [6]uint16
for i, cas := range cases {
    c := scase{}
    c.kind = ...
    c.elem = ...
    c.c = ...
}
chosen, revcOK := selectgo(selv, order, 3)
if chosen == 0 {
    ...
    break
}
if chosen == 1 {
    ...
    break
}
if chosen == 2 {
    ...
    break
}

展开后的代码片段中最重要的就是用于选择待执行case的运行时函数runtime.selectgo,这也是我们要关注的重点。因为这个函数的实现比较复杂,所以这里分两部分分析它的执行过程:

  1. 执行一些必要的初始化操作并确定case的处理顺序
  2. 在循环中根据case的类型做出不同的处理

初始化

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

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
	cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
	order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
	
	ncases := nsends + nrecvs
	scases := cas1[:ncases:ncases]
	pollorder := order1[:ncases:ncases]
	lockorder := order1[ncases:][:ncases:ncases]

	norder := 0
	for i := range scases {
		cas := &scases[i]
	}

	for i := 1; i < ncases; i++ {
		j := fastrandn(uint32(i + 1))
		pollorder[norder] = pollorder[j]
		pollorder[j] = uint16(i)
		norder++
	}
	pollorder = pollorder[:norder]
	lockorder = lockorder[:norder]

	// 根据 Channel 的地址排序确定加锁顺序
	...
	sellock(scases, lockorder)
	...
}

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

  • 轮询顺序:通过runtime.fastrandn函数引入随机性
  • 加锁顺序:按照channel的地址排序后确定加锁顺序

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

循环

当我们为select语句锁定了所有channel之后就会进入runtime.selectgo函数的主循环,它会分三个阶段查找或者等待某个channel准备就绪:

  1. 查找是否已经存在准备就绪的channel,即可以执行收发操作
  2. 将当前goroutine加入channel对应的收发队列上并等待其他goroutine的唤醒
  3. 当前goroutine被唤醒之后找到满足条件的channel并进行处理

runtime.selectgo函数会根据不同情况通过goto语句跳转到函数内部不同标签执行相应的逻辑,包括:

  • bufrecv:可以从缓冲区读取数据
  • bufsend:可以向缓冲区写入数据
  • recv:可以从休眠的发送方获取数据
  • send:可以向休眠的接收方发送数据
  • rclose:可以从关闭的channel读取EOF
  • sclose:向关闭的channel发送数据
  • retc:结束调用并返回

我们先来分析循环执行的第一个阶段,查找已经准备就绪的 Channel。循环会遍历所有的 case 并找到需要被唤起的 runtime.sudog 结构,在这个阶段,我们会根据 case 的四种类型分别处理:

  1. 当 case 不包含 Channel 时;
  • 这种 case 会被跳过;
  1. 当 case 会从 Channel 中接收数据时;
    • 如果当前 Channel 的 sendq 上有等待的 Goroutine,就会跳到 recv 标签并从缓冲区读取数据后将等待 Goroutine 中的数据放入到缓冲区中相同的位置;
    • 如果当前 Channel 的缓冲区不为空,就会跳到 bufrecv 标签处从缓冲区获取数据;
    • 如果当前 Channel 已经被关闭,就会跳到 rclose 做一些清除的收尾工作;
  2. 当 case 会向 Channel 发送数据时;
    • 如果当前 Channel 已经被关,闭就会直接跳到 sclose 标签,触发 panic 尝试中止程序;
    • 如果当前 Channel 的 recvq 上有等待的 Goroutine,就会跳到 send 标签向 Channel 发送数据;
    • 如果当前 Channel 的缓冲区存在空闲位置,就会将待发送的数据存入缓冲区;
  3. 当 select 语句中包含 default 时;
    • 表示前面的所有 case 都没有被执行,这里会解锁所有 Channel 并返回,意味着当前 select 结构中的收发都是非阻塞的;

第一阶段的主要职责是查找所有 case 中是否有可以立刻被处理的 Channel。无论是在等待的 Goroutine 上还是缓冲区中,只要存在数据满足条件就会立刻处理,如果不能立刻找到活跃的 Channel 就会进入循环的下一阶段,按照需要将当前 Goroutine 加入到 Channel 的 sendq 或者 recvq 队列中:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
	...
	gp = getg()
	nextp = &gp.waiting
	for _, casei := range lockorder {
		casi = int(casei)
		cas = &scases[casi]
		c = cas.c
		sg := acquireSudog()
		sg.g = gp
		sg.c = c

		if casi < nsends {
			c.sendq.enqueue(sg)
		} else {
			c.recvq.enqueue(sg)
		}
	}

	gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
	...
}

除了将当前 Goroutine 对应的 runtime.sudog 结构体加入队列之外,这些结构体都会被串成链表附着在 Goroutine 上。在入队之后会调用 runtime.gopark 挂起当前 Goroutine 等待调度器的唤醒。

等到 select 中的一些 Channel 准备就绪之后,当前 Goroutine 就会被调度器唤醒。这时会继续执行 runtime.selectgo 函数的第三部分,从 runtime.sudog 中读取数据:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
	...
	sg = (*sudog)(gp.param)
	gp.param = nil

	casi = -1
	cas = nil
	sglist = gp.waiting
	for _, casei := range lockorder {
		k = &scases[casei]
		if sg == sglist {
			casi = int(casei)
			cas = k
		} else {
			c = k.c
			if int(casei) < nsends {
				c.sendq.dequeueSudoG(sglist)
			} else {
				c.recvq.dequeueSudoG(sglist)
			}
		}
		sgnext = sglist.waitlink
		sglist.waitlink = nil
		releaseSudog(sglist)
		sglist = sgnext
	}

	c = cas.c
	goto retc
	...
}

第三次遍历全部 case 时,我们会先获取当前 Goroutine 接收到的参数 sudog 结构,我们会依次对比所有 case 对应的 sudog 结构找到被唤醒的 case,获取该 case 对应的索引并返回。

由于当前的 select 结构找到了一个 case 执行,那么剩下 case 中没有被用到的 sudog 就会被忽略并且释放掉。为了不影响 Channel 的正常使用,我们还是需要将这些废弃的 sudog 从 Channel 中出队。

selectgo 会根据变量 cas 的值来判断是收发操作唤醒还是关闭操作唤醒关闭操作唤醒的话 gp.param 会被置为 nil,那么就不会赋值 cas 变量

关闭操作唤醒 selectgo,并不一定会选择该 case对于关闭操作唤醒,逻辑会回到 loop 中再次执行 scases 的检查操作

关闭操作唤醒 selectgo 后,在完成所有 channel 加锁前又有 channel 准备好收发操作了,那么在 loop 查询时,按照 pollorder 随机顺序,可能会选中刚刚准备好的 channel,而不是唤醒 selectgo 的 case

收发操作唤醒 selectgo, 必定会选择该 case,而对于收发操作,已经完成了值的拷贝,必然会选择这个 case,而不会再次去查询

标签:case,语言,channel,selectgo,select,go,runtime,Channel
From: https://www.cnblogs.com/zpf253/p/17447241.html

相关文章

  • 高性能 Go 的 6 个技巧 — Go 高级主题
    本文旨在讨论6个提示,这些提示可以帮助诊断和修复Go应用程序中的性能问题。基准测试:在Go中编写有效的基准测试对于了解代码性能至关重要。可以通过将文件命名为“_test.go”,并使用testing包的Benchmark函数来创建基准测试。以下是一个示例:funcfibonacci(nint)int{ ifn<=......
  • c语言值得注意的知识
    1.说明下列每对scanf格式串是否等价?如果不等价,请指出它们的差异。(c)"%f"与"%f "。在`scanf`函数中,`"%f"`和`"%f"`这两种格式的区别在于后面的空格。1.`scanf("%f",&variable);`这种情况下,`scanf`会读取并解析用户输入的浮点数,然后将解析的值存入`variable`中。......
  • golang之recover
    recover是什么golang的recover是一个内置函数,用于在发生panic时恢复程序的控制流。当程序发生panic时,程序会停止执行当前的函数,并向上层函数传递panic,直到被recover函数捕获。recover函数必须在defer语句中调用,否则无法捕获panic。如果没有发生panic或者没有被recover函数捕获,程序......
  • Go-Map相关
    Go中map默认不安全的,也实现了并发安全的对象:sync.Map并发不安全不安全是因为源码中没有实现读写分离。进行了判断异常:在哈希表写操作时,会将哈希表的标志位 hashWriting 设置为1,以表明当前正在执行写操作。当其他协程执行哈希表的读操作时,会根据当前的标志位判断是否能够......
  • 1008.Django项目用户功能之docker
    docker跟virtualbox一样:是一个虚拟软件,可以创建多个程序的运行环境。docker与virtualbox的差别:docker不会虚拟出自己的内核,而是直接使用宿主机的内核。为什么要用docker? 集群:分布式相关的环境使用和部署mysql长沙 mysql北京 mysql上海 数据同步,可以相互提供数据服务,而......
  • Google Pixel 4 Android13 刷入Magisk + KernelSU 双root环境
    本文所有教程及源码、软件仅为技术研究。不涉及计算机信息系统功能的删除、修改、增加、干扰,更不会影响计算机信息系统的正常运行。不得将代码用于非法用途,如侵立删!GooglePixel4Android13刷入Magisk+KernelSU双root环境环境win10Pixel4Android13下载官方rom包......
  • mongodb压缩——snappy、zlib块压缩,btree索引前缀压缩
    MongoDB3.0WiredTigerCompressionandPerformanceOneofthemostexcitingdevelopmentsoverthelifetimeofMongoDBmustbetheinclusionoftheWiredTigerstorageengineinMongoDB3.0.Itsverydesignandcorearchitecturearelegionsaheadofthecurr......
  • golang实现设计模式之抽象工厂模式总结-代码、优缺点、适用场景
    抽象工厂模式也是一种创建型的设计模式,其是在工厂模式的基础上实现更高程度的内聚。我们知道在工厂模式中,一种产品类就需要新建个对应的工厂类生成产品的实例,这会有什么问题呢?虽然工厂模式解决了简单工厂模式不好扩展的问题,实现了OCP,但一种产品就需要新建一个工厂类,比如有10000种......
  • Gorm - 使用gorm时进行执行自定义SQL的几种方式
    1、当只需要执行某个SQL而不需要进行获取返回值时//如果其中有变量,则使用?进行占位,sql:="要执行的SQL"//在Exec方法中在sql后面可以使用多个参数作为占位的补充//例如需要name=?,则写法可以使用util.Db.Exec(sql,"张三").Errorerr:=util.Db.Exec......
  • golang实现设计模式之工厂模式总结-代码、优缺点、适用场景
    工厂模式也是一种创建型模式,它与简单工厂不同的是将实例的创建推迟到具体的工厂类方法中实现,每一种产品生成一个对应的工厂,从而替换掉简单工厂方法模式中那个静态工厂方法。所以在工厂模式中,不同产品就由不同的工厂生产,每次增加产品时,我们就不需要在类似在简单工厂中,在统一的工厂......