美文网首页Golang
golang之channel

golang之channel

作者: 神奇的考拉 | 来源:发表于2019-04-09 16:24 被阅读11次

    前言

    本文算是对Diving Deep Into The Golang Channels的翻译,也算加强对channel的了解和使用。

    使用channel

    func goRoutineA(a <- chan int){
      val := <- a
      fmt.Println("goRoutineA received the data", val)
    }
    
    func main(){
      ch := make(chan int) // 定义一个channel:接收int类型
      go goRoutineA(ch)
      time.Sleep(time.Second * 1)  // 防止主线程退出看不到goroutine内容输出
    }
    

    整个执行流程如下


    channel-1
    channel-2

    从上面两张图片看到:使用make(chan int)定义的channel,当channel中不存在数据时 在执行<- a时会被blocked直到channel中有数据。
    在golang中使用channel能够使得runnable的goroutine在向channel发送或接收数据时处于blocked。

    channel structure

    在go中,channel实现了在不同的goroutine间传递message的基本。
    当我们使用make函数来创建channel后,对应的结构应该是怎样的?

    ch := make(chan int, 3)
    
    在runtime时channel具体结构

    接下来我们会针对其中的一些内容进行详解

    hchan struct

    当我们通过make(chan int, 3)创建一个buffer=3的channel时,就会创建一个hchan结构


    hchan
    • dataqsize: 对应的channel的buffer大小,比如使用make(chan T, N),其中T代表channel中元素的类型,N就是channel的buffer大小;
    • elementsize:channel中的元素大小;
    • buf:channel中element真正存放的循环队列;不过该字段只有在使用buffered的channel时才有意义;
    • closed:记录当前channel是否已关闭,在使用make创建一个channel后,closed=0, 代表当前channel处于open;当调用close时可将该channel关闭,closed=1;代表当前channel不能再进行任何write操作。
    • recvq 和 sendq:都是等待队列,主要存放进行读取channel数据或写入channel数据时处于blocked的goroutines。
    • lock:主要用来保证channel的读写或发送接收是互斥操作。确保对channel的读取或写入的阻塞。

    sudog struct

    可将sudgo当成goroutine来理解


    sudog结构

    先将前面的实例进行调整下:

    func goRoutineA(a <- chan int){
      val := <- a
      fmt.Println("goroutineA received the data", val)
    }
    
    func goRoutineB(b <- chan int){
      val := <- b
      fmt.Println("goroutineB received the data ", val)
    }
    
    func main(){
      ch := make(chan int)
      go goRoutineA(ch)
      go goRoutineB(ch)
       ch <- 3  
      time.Sleep(time.Second * 1)
    }
    

    对应的生成的channel结构如下:


    channel结构

    可以看到凸出部分展示了本实例中定义两个goroutine(goroutineA和goroutineB)来尝试读取channel中的数据。在执行 ch <- 3之前,由于channel中并没有任何数据,而两个goroutine将会阻塞在接收数据操作上,并用sudog进行包装,同时两个sudog会被存放到recvq里。
    在channel中的recvq和sendq都是基于链表实现的,如下


    channel之recvq
    对于channel的sendq类似,此处不再累述。接下来看看当执行ch <-3发生了什么?

    channel之send操作: c<- x

    先看看如下几种send操作:

    • 1.对nil channel执行send操作
    nil channel执行send

    在对一个nil channel执行send操作时 会导致当前goroutine暂停其操作

    • 2.对closed channel执行send
    closed channel

    向一个已经closed的channel发送数据会触发一个panic

    • 3.当一个goroutine阻塞在channel上,send数据时会直接将数据发送该goroutine
    blocked channel

    该实例也说明recvq在其中扮演一个最终的角色:若是在recvq中任意一个goroutine在等待接收数据,对应的channel的wirter会直接将value传递给当前的goroutine(waiting receiver)。见send函数:


    channel之send

    在396行代码处 goready(gp, skip + 1),会使得在阻塞等待数据的那个goroutine将被再次runnable,go scheduler也将会再次运行该goroutine。

    • 4.Buffered Channel

    当我们通过make(chan T, N)定义一个带有buffer的channel时,若是对应的hchan.buf还有可用空间则会将data存到到buffer中而不是像非buffered的channel一样处于阻塞,等待数据被接收。


    buffered channel

    chanbuf(c, i)直接访问相应的内存空间。
    通过对比qcount和dataqsiz来判断hchan.buf是否还有free空间;通过将ep指针指向的区域copy到ringbuffer,来完成入列元素的send操作,并调整sendx和qcount。

    • 5.若是hchan.buf没有可用空间时 会如何???
    full channel

    上述代码:
    首先会在当前stack上创建一个goroutine,并将该goroutine状态=park同时将该goroutine添加到sendq中。

    关于send

    1.将当前的channel进行blocked
    2.确定执行write,会从recvq中获取一个等待的goroutine,并将对应的element直接写给该goroutine。
    3.当对应的recvq是空的,首先要确保当前的buffer是否可用,若是可用,则从当前的goroutine的copy数据到buffer中
    typedmemmove内部使用memmove将一个内存块从一个位置copy到另外一个位置。
    4.若是buffer已满,则写入到channel的元素会被保存到当前运行的goroutine,并且当前goroutine将sendq处进行等待。
    通过对比buffered channel和unbuffered channel差别在于对应的hchan分配有buffer。对于一个unbuffered channel 当send数据时并没有对应的receiver则会将元素保存到sudog中的elem字段,对应buffered channel也是同样的道理。
    接下来会通过结合实例来阐述关于上面罗列的第4点:
    如下代码只是用来演示 执行可能会导致一个panic

    package main
    
    func goroutineA(c2 chan int){
      c2 <- 2
    }
    
    func main(){
      c2 := make(chan int)
      go routineA(c2)
    
      for{}
    }
    

    如上的运行时channel的结构


    unbuffered

    不过即使我们将值2添加到channel中对应的buf却不存在该值,将会保存在goroutine的sudog结构中。在上面例子中goroutineA向channel c2发送数据,但此时并没有对应的receiver准备接收数据,因而goroutineA将被添加到channel的sendq列表中,并一直阻塞暂停等待receiver来获取数据。接下来看看运行时的sendq结构,来验证前面的内容


    runtime sendq
    这样在实例代码中 ch <- 2后具体发生的事宜。
    而对于recvq来说如果存在等待状态的goroutine,它获取queue的第一个sudog并将数据放到goroutine中。

    针对channel所有的transfer都是采用值copy的方式。也就是说在channel的所有的操作都是值拷贝。


    值拷贝

    正如上面演示样例 也是通过拷贝g的值到buffer中。
    Don't communicate by sharing memory; share memory by communicating.

    &{Ankur 25}
    modifyUser Received Value &{Ankur Anand 100}
    printUser goRoutine called &{Ankur 25}
    &{Anand 100}
    
    样例值拷贝

    receive channel

    其实跟channel send操作很类似。


    channel receiver

    Select: 多路复用

    演示实例


    multiplexing on multiple channel

    1.在select代码块中的case执行都是互斥的,故而是需要select case中的channel来获取lock执行的,每个channel获取执行lock的顺序是基于Hchan地址的排序来进行lock的获取,这样就能确保不会同时锁定所有相关通道的互斥锁。

    sellock(scases, lockorder)
    

    每个在scases数组中的scase包括当前case的操作类型以及它所在的channel。


    scase结构
    • kind 代表当前case的操作类型,可能取值:CaseRecv、CaseSend、CaseDefault
      2.计算轮询顺序:shuffle所有涉及的通道,以提供伪随机保证,并根据轮询顺序依次遍历所有情况,以查看其中是否有准备好进行通信。这个轮询顺序使得select操作不必遵循程序中声明的顺序。


      poll order
      case in select

      3.在select代码块中,只要有一个通道操作没有阻塞,select语句就可以返回,如果选择的通道已经准备好了,甚至不需要接触所有通道。
      若是当前没有通道响应,也没有默认语句,则当前g必须根据情况挂载所有通道的相应等待队列。
      若是当前所有的case都已准备好, 则会随机执行一个case。


      park goroutine in select case
    • sg.isSelect 代表goroutine正在参与当前的select块。

    channel是go中一个非常强大和有趣的机制。但是为了有效地使用它们,你必须了解它们是如何工作的。希望本文能够解释Go中通道所涉及的非常基本的工作原理。

    最后推荐Go Study Group 欢迎加入。

    相关文章

      网友评论

        本文标题:golang之channel

        本文链接:https://www.haomeiwen.com/subject/fizviqtx.html