golang之channel

前言

本文算是对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内容输出
}

整个执行流程如下


golang之channel_第1张图片
channel-1

golang之channel_第2张图片
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)
golang之channel_第3张图片
在runtime时channel具体结构

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

hchan struct

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


golang之channel_第4张图片
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来理解


golang之channel_第5张图片
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结构如下:


golang之channel_第6张图片
channel结构

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


golang之channel_第7张图片
channel之recvq

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

channel之send操作: c<- x

先看看如下几种send操作:

  • 1.对nil channel执行send操作
golang之channel_第8张图片
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函数:


golang之channel_第9张图片
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一样处于阻塞,等待数据被接收。


golang之channel_第10张图片
buffered channel

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

  • 5.若是hchan.buf没有可用空间时 会如何???
golang之channel_第11张图片
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的结构


golang之channel_第12张图片
unbuffered

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


golang之channel_第13张图片
runtime sendq

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

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


golang之channel_第14张图片
值拷贝

正如上面演示样例 也是通过拷贝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}
golang之channel_第15张图片
样例值拷贝

receive channel

其实跟channel send操作很类似。


golang之channel_第16张图片
channel receiver

Select: 多路复用

演示实例


golang之channel_第17张图片
multiplexing on multiple channel

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

sellock(scases, lockorder)

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


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


    golang之channel_第19张图片
    poll order

    golang之channel_第20张图片
    case in select

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


    golang之channel_第21张图片
    park goroutine in select case
  • sg.isSelect 代表goroutine正在参与当前的select块。

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

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

你可能感兴趣的:(golang之channel)