zoukankan      html  css  js  c++  java
  • go select原理

    1. 前言

    select是Golang在语言层面提供的多路IO复用的机制,可以检测多个channel是否准备就绪(即是否可读或可写)。

    本节试图通过源码总结其实现原理,从而发现一些使用误区或解释一些不常见的现象

    2. 一些常见使用场景

    1. 当程序中使用多个case语句时,会随机选择一个进行执行

      package main
      
      import (
          "fmt"
          "time"
      )
      
      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.")
      }
      

      代码中,声明两个channel, chan1,chan2, 一次启动两个goroutine,分别向两个chan中写入一个数据后sleep。select语句的两个case分别坚持chan1,chan2是否可读,default用来执行默认语句

      多次运行发现:select 中各个case执行顺序时随机的 , 如果某个case中的channel已经ready,则执行相应的语句并退出select流程

      如果所有case中的channel都未ready,则执行default中的语句然后退出select流程。

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

      可能的输出:

      # 可能的输出1
      chan1 ready.
      main exit.
      
      # 可能的输出2
      chan2 ready.
      main exit.
      
      # 可能的输出3
      default
      main exit.
      
    2. 当select多个case,没有default分支时,如果每一个case都没有准备好,select语句将处于永远阻塞状态。

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          chan1 := make(chan int)
          chan2 := make(chan int)
      
          writeFlag := false
          go func() {
             
              for {
                   // 由于withFlag永远为false,所以chan1 永远不会有值写入
                  if writeFlag {
                      chan1 <- 1
                  }
                  time.Sleep(time.Second)
              }
          }()
      
          go func() {
              for {
                  if writeFlag {
                      chan2 <- 1
                  }
                  time.Sleep(time.Second)
              }
          }()
      
          select {
          // chan虽然准备好了接收操作,但是由于无数据写入,所以次case处于阻塞
          case <-chan1: 
              fmt.Println("chan1 ready.")
          // chan2同1一样,也处于阻塞
          case <-chan2:
              fmt.Println("chan2 ready.")
          }
      
          fmt.Println("main exit.")
      }
      

      说明: 见代码注释,程序会一直处于阻塞状态,没有任何输出

    3. 关闭通道后,也能继续读取通道里的数据,不会报错,对于有缓存的通道channel,读取的是最先传入的数据,对于无缓冲的通道如果缓冲种没有数据,会读取通道类型的零值。

      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.")
          }
      

      说明:会随机输出

    4. 当程序种没有协程运行时空的select会引起异常,如果有协程,则可以阻止程序退出,使程序处于挂起状态

      package main
      
      func main() {
         
          select {
          }
      }
      

      说明:程序会报错

      package main
      
      import "fmt"
      import "time"
      
      func main() {
          go func(){
              count:=1
              for {
                  count++
                  time.sleep(time.Second)
                  fmt.Println("count:",count)
              }
          }()   
          select {}
      }
      

      说明:程序不会退出,程序以deamon模式一直后台运行

    3. 实现原理

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

    3.1程序结构

    1. 源码包src/runtime/select.go:scase定义了表示case语句的数据结构:

      type scase struct {
          c           *hchan         // chan
          kind        uint16
          elem        unsafe.Pointer // data element
      }
      
    2. scase.c为当前case语句所操作的channel指针,这也说明了一个case语句只能操作一个channel。

    3. scase.kind表示该case的类型,分为读channel、写channel和default,三种类型分别由常量定义:

      • caseRecv:case语句中尝试读取scase.c中的数据;
      • caseSend:case语句中尝试向scase.c中写入数据;
      • caseDefault: default语句
    4. scase.elem表示缓冲区地址,根据scase.kind不同,有不同的用途:

      • scase.kind == caseRecv : scase.elem表示读出channel的数据存放地址;
      • scase.kind == caseSend : scase.elem表示将要写入channel的数据存放地址;

    3.2 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数组的长度

    函数返回值:

    1. int: 选中case的编号, 这个case编号跟代码一致
    2. 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。

    4. 总结

    1. select语句中除default外,每个case操作一个channel,要么读要么写
    2. select语句中除default外,各case执行顺序是随机的
    3. select语句中如果没有default语句,则会阻塞等待任一case
    4. select语句中读操作要判断是否成功读取,关闭的channel也可以读取
    ♥永远年轻,永远热泪盈眶♥
  • 相关阅读:
    jquery水印插件:placeholder
    EF POWER TOOLS由数据库逆向CODE FIRST
    .NET重构(类型码的设计、重构方法)
    CodeUI Test:创建第一个CodeUI Test
    Windows 8 Store Apps
    ASP.NET MVC 使用MSBuild部署的几个注意事项
    c#中如何跨线程调用windows窗体控件
    Restful?
    Javascript的一种代码结构方式——插件式
    AOP编程
  • 原文地址:https://www.cnblogs.com/failymao/p/14932781.html
Copyright © 2011-2022 走看看