前言
本文算是对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内容输出
}
整个执行流程如下
从上面两张图片看到:使用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)
接下来我们会针对其中的一些内容进行详解
hchan struct
当我们通过make(chan int, 3)创建一个buffer=3的channel时,就会创建一个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来理解
先将前面的实例进行调整下:
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结构如下:
可以看到凸出部分展示了本实例中定义两个goroutine(goroutineA和goroutineB)来尝试读取channel中的数据。在执行 ch <- 3之前,由于channel中并没有任何数据,而两个goroutine将会阻塞在接收数据操作上,并用sudog进行包装,同时两个sudog会被存放到recvq里。
在channel中的recvq和sendq都是基于链表实现的,如下
对于channel的sendq类似,此处不再累述。接下来看看当执行ch <-3发生了什么?
channel之send操作: c<- x
先看看如下几种send操作:
-
1.对nil channel执行send操作
在对一个nil channel执行send操作时 会导致当前goroutine暂停其操作
-
2.对closed channel执行send
向一个已经closed的channel发送数据会触发一个panic
-
3.当一个goroutine阻塞在channel上,send数据时会直接将数据发送该goroutine
该实例也说明recvq在其中扮演一个最终的角色:若是在recvq中任意一个goroutine在等待接收数据,对应的channel的wirter会直接将value传递给当前的goroutine(waiting receiver)。见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一样处于阻塞,等待数据被接收。
chanbuf(c, i)直接访问相应的内存空间。
通过对比qcount和dataqsiz来判断hchan.buf是否还有free空间;通过将ep指针指向的区域copy到ringbuffer,来完成入列元素的send操作,并调整sendx和qcount。
-
5.若是hchan.buf没有可用空间时 会如何???
上述代码:
首先会在当前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的结构
不过即使我们将值2添加到channel中对应的buf却不存在该值,将会保存在goroutine的sudog结构中。在上面例子中goroutineA向channel c2发送数据,但此时并没有对应的receiver准备接收数据,因而goroutineA将被添加到channel的sendq列表中,并一直阻塞暂停等待receiver来获取数据。接下来看看运行时的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操作很类似。
Select: 多路复用
演示实例
1.在select代码块中的case执行都是互斥的,故而是需要select case中的channel来获取lock执行的,每个channel获取执行lock的顺序是基于Hchan地址的排序来进行lock的获取,这样就能确保不会同时锁定所有相关通道的互斥锁。
sellock(scases, lockorder)
每个在scases数组中的scase包括当前case的操作类型以及它所在的channel。
-
kind 代表当前case的操作类型,可能取值:CaseRecv、CaseSend、CaseDefault
2.计算轮询顺序:shuffle所有涉及的通道,以提供伪随机保证,并根据轮询顺序依次遍历所有情况,以查看其中是否有准备好进行通信。这个轮询顺序使得select操作不必遵循程序中声明的顺序。
3.在select代码块中,只要有一个通道操作没有阻塞,select语句就可以返回,如果选择的通道已经准备好了,甚至不需要接触所有通道。
若是当前没有通道响应,也没有默认语句,则当前g必须根据情况挂载所有通道的相应等待队列。
若是当前所有的case都已准备好, 则会随机执行一个case。
- sg.isSelect 代表goroutine正在参与当前的select块。
channel是go中一个非常强大和有趣的机制。但是为了有效地使用它们,你必须了解它们是如何工作的。希望本文能够解释Go中通道所涉及的非常基本的工作原理。