ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

go底层系列-select原理剖析

2021-12-12 09:31:08  阅读:134  来源: 互联网

标签:case 语句 int 剖析 select go scase channel


select

文章目录

前言

  • select是Golang在语言层面提供的多路IO复用的机制
  • 可以检测多个channel是否ready(即是否可读或可写)
  • 使用起来非常方便

热身

// 下面的程序输出是什么?
func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		chan1 <- 1
		time.Sleep(5 * time.Second)
	}()
	go func() {
		chan2 <- 1
		time.Sleep(5 * time.Second)
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	default:
		fmt.Println("default")
	}
	fmt.Println("main exit.")
}

// 参考答案:select中各个case执行顺序是随机的,如果某个case中的channel已经ready,则执行相应的语句并退出select流程
// 如果所有case中的channel都未ready
//		则执行default中的语句
//		然后退出select流程
// 另外,由于启动的协程和select语句并不能保证执行顺序,所以也有可能select执行时协程还未向channel中写入数据,
// 所以select直接执行default语句并退出。所以,三种输出都有可能



// 下面的程序执行到select时会发生什么?
func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	writeFlag := false

	go func() {
		for {
			if writeFlag {
				chan1 <- 1
			}
			time.Sleep(time.Second)
		}
	}()

	go func() {
		for {
			if writeFlag {
				chan2 <- 1
			}
			time.Sleep(time.Second)
		}
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	}
	fmt.Println("main exit.")
}
// 参考答案:select会按照随机的顺序检测各case语句中channel是否ready
// 如果某个case中的channel已经ready,则执行相应的case语句
// 然后退出select流程
// 如果所有的channel都未ready且没有default的话
// 则会阻塞等待各个channel。
// 所以上述程序会一直阻塞。




// 下面程序有什么问题?
func main() {
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() {
		close(chan1)
	}()

	go func() {
		close(chan2)
	}()

	select {
	case <-chan1:
		fmt.Println("chan1 ready.")
	case <-chan2:
		fmt.Println("chan2 ready.")
	}
	fmt.Println("main exit.")
}
// 参考答案:
// select会按照随机的顺序检测各case语句中channel是否ready
// 考虑到已关闭的channel也是可读的,所以上述程序中select不会阻塞
// 具体执行哪个case语句具是随机的

// 下面程序会发生什么?
func main() {
	select {
	}
}
// 参考答案:
//	对于空的select语句,程序会被阻塞,准确的说是当前协程被阻塞
// 	同时Golang自带死锁检测机制,当发现当前协程再也没有机会被唤醒时,则会panic。
// 	所以上述程序会panic

实现原理

  • Golang实现select时,定义了一个数据结构表示每个case语句(含defaut,default实际上是一种特殊的 case)
  • select执行过程可以类比成一个函数
    • 输入case数组
    • 输出选中的case
    • 然后程序流程转到选中的 case块

case数据结构

在这里插入图片描述

  • scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel
  • scase.kind 表示该case的类型
    • 分为读channel、写channel和default
    • 三种类型分别由常量定义
      • caseRecv:case语句中尝试读取scase.c中的数据
      • caseSend:case语句中尝试向scase.c中写入数据
      • caseDefault: default语句
  • scase.elem表示缓冲区地址
    • 跟据scase.kind不同,有不同的用途:
      • scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址
      • scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;

select实现逻辑

// 源码包 src/runtime/select.go:selectgo() 定义了select选择case的函数:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
  • 函数参数:

    • cas0为scase数组的首地址
      • selectgo()就是从这些scase中找出一个返回
    • order0为一个两倍cas0数组长度的buffer
      • 保存scase随机序列pollorder
      • scase中channel地址序列 lockorder
        • pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的
        • lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的
    • ncases表示scase数组的长度
  • 函数返回值:

    • int: 选中case的编号
      • 这个case编号跟代码一致
    • bool: 是否成功从channle中读取了数据
      • 如果选中的case是从channel中读数据
        • 则该返回值表示是否读 取成功。
selectgo实现伪代码如下:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
	//1. 锁定scase语句中所有的channel
	//2. 按照随机顺序检测scase中的channel是否ready
	// 		2.1 如果case可读,则读取channel中数据,解锁所有的channel,然后返回(case index, true)
	// 		2.2 如果case可写,则将数据写入channel,解锁所有的channel,然后返回(case index, false)
	// 		2.3 所有case都未ready,则解锁所有的channel,然后返回(default index, false)
	//3. 所有case都未ready,且没有default语句
	// 		3.1 将当前协程加入到所有channel的等待队列
	// 		3.2 将当前协程转入阻塞,等待被唤醒
	//4. 唤醒后返回channel对应的case index
	// 		4.1 如果是读操作,解锁所有的channel,然后返回(case index, true)
	// 		4.2 如果是写操作,解锁所有的channel,然后返回(case index, false)
}
  • 特别说明:
    • 对于读channel的case来说,如 case elem, ok := <-chan1: ,
      • 如果channel有可能被其他协程关闭的情 况下
      • 一定要检测读取是否成功
      • 因为close的channel也有可能返回
      • 此时ok == false

总结

  • select语句中除default外,每个case操作一个channel,要么读要么写
  • select语句中除default外,各case执行顺序是随机的
  • select语句中如果没有default语句,则会阻塞等待任一case
  • select语句中读操作要判断是否成功读取,关闭的channel也可以读取

标签:case,语句,int,剖析,select,go,scase,channel
来源: https://blog.csdn.net/wys74230859/article/details/121879389

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有