go select 原理

原创
02/01 14:21
阅读数 92

概述

go 的 select 语句是专门为了 channel 发送和接收消息而诞生的语句, 在语句的运行期间, 该 goroutine 是阻塞的.

DEMO

func main() {
	ch1 := make(chan int, 1)
	ch2 := make(chan int, 10)
	select {
	case v := <-ch1:
		fmt.Printf("ch1 = %v", v)
	case v := <-ch2:
		fmt.Printf("ch2 = %v", v)
	default:
		// 如果没有 default, 会一直阻塞等待某个 case 成功
		fmt.Println("default!")
	}
}

注意两个问题:

select 并不是一个循环, 如果你需要反复的监听多个 channel, 搭配 for{}使用,如果外层没有for,将只会执行一次就结束

常用的方式如下

for {
  select {
    case v := <-ch1:
    fmt.Printf("ch1 = %v", v)
    case v := <-ch2:
    fmt.Printf("ch2 = %v", v)
  }
  // 一次读取之后立刻再次监听
}

实现原理

数据结构

select 底层由两部分组成, case 语句和执行函数,每一个 case 语句结构如下

type scase struct {
    c           *hchan         // chan
    elem        unsafe.Pointer // 读或者写的缓冲区地址
}

这里的 hchan, 存放了监听的 channel, 在一个 select 中, 包含了多个 case. 这些 case 组成了一个数组

执行的 select 语句, 实际上调用函数func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) 源码位置

参数意义如下:

  • cas0: case 数组中第一个case的地址
  • order0: case数组两倍长的缓冲区
  • ncases: case 数组的长度
  • selectgo 返回的说所选的 scase 的索引, 而如果 scase 是接收操作, 则返回是否收到值

调用流程

我们在运行一个 select 时, 函数的调用顺序如下

  • func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
  • func rselect([]runtimeSelect) (chosen int, recvOK bool) func
  • selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)

前两个都是简单的初始化参数, 重点其实就在selectgo里

selectgo 的处理流程如下:

根据 cas0 获取 case 数组

将 case 数组顺序打乱

将 case 数组内的每个 chan 全部上锁

// lock all the channels involved in the select
	sellock(scases, lockorder)

遍历所有的 case 数组元素

查看其是否可读和可写,并且直接goto到相应处理模块

	for _, casei := range pollorder {
		casi = int(casei)
		cas = &scases[casi]
		c = cas.c

		if casi >= nsends {
			sg = c.sendq.dequeue()
			if sg != nil {
				goto recv
			}
			if c.qcount > 0 {
				goto bufrecv
			}
			if c.closed != 0 {
				goto rclose
			}
		} else {
			if raceenabled {
				racereadpc(c.raceaddr(), casePC(casi), chansendpc)
			}
			if c.closed != 0 {
				goto sclose
			}
			sg = c.recvq.dequeue()
			if sg != nil {
				goto send
			}
			if c.qcount < c.dataqsiz {
				goto bufsend
			}
		}
	}

如果没有case,解锁所有

	selunlock(scases, lockorder)
	goto retc

如果有可读或可写 case, 解锁所有的 chan, 返回对应的 chan 数据,比如 goto recv,

如果没有配合for的话,select就此结束(goto ret)

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部