二. go 常见控制结构实现原理之 select

一. 基础问题

  1. select是Golang在语言层面提供的多路IO复用的机制,其可以检测多个channel是否ready(即是否可读或可写)
  2. 示例与问题:
  1. select会按照随机的顺序检测各case语句中channel是否ready,
  2. 如果某个case中的channel已经ready则执行相应的case语句然后退出select流程,
  3. 如果所有的channel都未ready且没有default的话,则会阻塞等待各个channel。
  4. 由于writeFlag一直是false, 不会向声明的channel中添加数据,一直没有reday,所以会一直阻塞
package main
import (
    "fmt"
    "time"
)
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)
        }
    }()
	//如果上方有channel添加数据成功,就会执行相应的case,
	//按照协程调度的顺序打印其中一个case与main退出的日志,否则会一直阻塞
    select {
    case <-chan1:
        fmt.Println("chan1 ready.")
    case <-chan2:
        fmt.Println("chan2 ready.")
    }
    fmt.Println("main exit.")
}
  1. 示例与问题2
  1. select会按照随机的顺序检测各case语句中channel是否ready,
  2. 考虑到已关闭的channel也是可读的,所以上述程序中select不会阻塞,
  3. 具体执行哪个case语句具是随机的(协程调度)
package main
import (
    "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 ready.")
    case <-chan2:
        fmt.Println("chan2 ready.")
    }
    fmt.Println("main exit.")
}
  1. 示例与问题3

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

package main
func main() {
    select {
    }
}

select 与channel

select 与 channel

  1. select语句是Go语言中用于处理channel的一种机制,它允许在多个channel上等待数据,并在其中任意一个channel有数据可读时立即执行相应的操作。使用select语句可以实现非常灵活的并发编程,可以避免出现死锁和竞争条件等问题
  2. select语句由多个case分支组成,每个case表示一个channel的操作(包括发送和接收操作),当其中任意一个channel有数据可读时,就会执行对应的case分支,并终止select语句的执行。如果多个channel同时有数据可读,那么会随机选择一个channel并执行对应的case分支,select语句还支持default分支
  3. select读取channel中的数据有什么特点
  1. 阻塞式的等待: select 语句会阻塞当前 goroutine,直到至少有一个 channel 可用。如果没有任何 channel 可操作,则 select 语句会一直等待,直到有一个或多个 channel 可用。
  2. 非阻塞式的读取: 使用 select 语句读取 channel 的数据时,通过在 case 中添加 default 操作,在没有可操作的 channel 时立即执行 default 中的操作
    3.多路复用:使用select语句可以同时监听多个channel,并在其中任意一个channel中有数据可读时立即执行对应的case分支,从而实现多路复用的效果
  3. 随机选择: 当多个 channel 都可以操作时,select 语句会按照随机的顺序选择其中一个 case 进行处理。这个特点让 select 语句具有更好的灵活性

二. 实现原理

1. select 底层结构

  1. 先看一下case语句源码的数据结构
type scase struct {
    c           *hchan         // chan
    kind        uint16
    elem        unsafe.Pointer // data element
}
  1. 其中scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。scase.kind表示该case的类型,分为读channel、写channel和default
  2. 读channel、写channel和default,三种类型分别由常量定义:
  1. caseRecv:case语句中尝试读取scase.c中的数据;
  2. caseSend:case语句中尝试向scase.c中写入数据;
  3. caseDefault: default语句
  1. scase.elem表示缓冲区地址,跟据scase.kind不同,有不同的用途:
  1. scase.kind = caseRecv : scase.elem表示读出channel的数据存放地址;
  2. scase.kind = caseSend : scase.elem表示将要写入channel的数据存放地址

2. select选择case的执行逻辑

  1. 查看select选择case的函数源码:
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
  1. 其中入参:
  1. cas0为scase数组的首地址,selectgo()就是从这些scase中找出一个返回。
  2. order0为一个两倍cas0数组长度的buffer,保存scase随机序列pollorder和scase中channel地址序列lockorder
  3. order0中保存的pollorder:每次selectgo执行都会把scase序列打乱,以达到随机检测case的目的。
  4. order0中保存的lockorder:所有case语句中channel序列,以达到去重防止对channel加锁时重复加锁的目的。
  5. ncases表示scase数组的长度
  1. 反参:
  1. int: 选中case的编号,这个case编号跟代码一致
  2. bool: 是否成功从channle中读取了数据,如果选中的case是从channel中读数据,则该返回值表示是否读取成功
  1. 伪代码
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)
}
  1. 注意对于读channel的case来说,如case elem, ok := <-chan1:, 如果channel有可能被其他协程关闭的情况下,一定要检测读取是否成功,因为close的channel也有可能返回,此时ok == false
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值