Golang

golang之channel

2019-04-09  本文已影响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

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操作:

nil channel执行send

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

closed channel

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

blocked channel

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


channel之send

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

当我们通过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。

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结构

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

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

上一篇下一篇

猜你喜欢

热点阅读