channel 是 goroutine 之间通信的一种方式,可以类比成 Unix 中的进程的通信方式管道。
在讲 channel 之前,有必要先提一下 CSP 模型,传统的并发模型主要分为 Actor 模型和 CSP 模型,CSP 模型全称为 communicating sequential processes,CSP 模型由并发执行实体(进程,线程或协程),和消息通道组成,实体之间通过消息通道发送消息进行通信。和 Actor 模型不同,CSP 模型关注的是消息发送的载体,即通道,而不是发送消息的执行实体。关于 CSP 模型的更进一步的介绍,有兴趣的同学可以阅读论文 Communicating Sequential Processes,Go 语言的并发模型参考了 CSP 理论,其中执行实体对应的是 goroutine, 消息通道对应的就是 channel。
channel提供了一种通信机制,通过它,一个goroutine可以向另一个goroutine发送消息。channel本身还需关联了一个类型,也就是channel可以发送数据的类型。例如:发送int类型信息的channel写作chan int。
channel使用内置的make函数创建,下面声明了一个chan int类型的channel:
ch := make(chan int)
c和map类似,make创建了一个底层数据结构体的引用,当赋值或参数传递时,只是拷贝了一个channel引用,指向相同的channel对象。和其他引用类型一样,channel的空值为nil。使用==可以对类型相同的channel进行比较,只是指向相同对象或同为nil时,才返回true。
ch := make(chan int)
// write to channel
ch <- x
// read from channel
x <- ch
// another way to read
x = <- ch
channel一定要初始化后才能进行读写操作,否则会永久阻塞。
golang提供了内置的close函数对channel进行关闭操作。
ch := make(chan int)
close(ch)
有关channel的关闭,你需要注意以下事项:
ch := make(chan int, 10)
ch <- 11
ch <- 12
close(ch)
for x := range ch {
fmt.Println(x)
}
x, ok := <- ch
fmt.Println(x, ok)
-------
output:
11
12
0 false
channel分为不带缓存的channel和带缓存的channel。
从无缓存的channle中读取消息会阻塞,直到有goroutine向该channel中发送消息;同理,向无缓存的channel中发送消息也会阻塞,直到有goroutine从channel中读取消息。
通过无缓存的channel进行通信时,接收者收到数据happens before发送者goroutine唤醒
有缓存的channel的声明方式为指定make函数的第二个参数,该参数为channel缓存的容量
ch := make(chan int, 10)
有缓存的channel类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向channel中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他goroutine从中读取消息;相应的,当channel中消息不为空时,读取消息不会出现阻塞,当channel为空时,读取操作会造成阻塞,直到有goroutine向channel中写入消息。
ch := make(chan int, 3)
// blocked, read from empty buffered channel
<- ch
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// blocked, send to full buffered channel
ch <- 4
通过len函数可以获得chan中的元素个数,通过cap函数可以得到channel的缓存长度。
看一个effective go中的例子:
c := make(chan int) // Allocate a channel
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
list.Sort()
c <- 1 // Send a signal; value does not matter.
}()
doSomethingForAWhile()
<-c
主goroutine会阻塞,直到执行sort的goroutine完成。
channel也可以使用range取值,并且会一直从channel中读取数据,直到有goroutine对改channel执行close操作,循环才会结束。
// consumer worker
ch := make(chan int, 10)
for x := range ch {
fmt.Println(x)
}
等价于
for {
x, ok := <- ch
if !ok {
break
}
fmt.Println(x)
}
select用法类似于IO多路复用,可以同时监听多个channel的消息状态,看下面的例子
select {
case <- ch1;
...
case <- ch2;
...
case ch3 <- 10;
...
default:
...
}
ch := make(chan struct{})
// finish task while send msg to ch
go doTask(ch)
timeout := time.After(5 * time.Second)
select {
case <- ch:
fmt.Println("task finished.")
case <- timeout:
fmt.Println("task timeout.")
}
有一些场景中,一些worker goroutine需要一直循环处理信息,直到收到quit信号
msgCh := make(chan struct{})
quitCh := make(chan struct{})
for {
select {
case <- msgCh:
doWork()
case <- quitCh:
finish()
return
}
即只可写入或只可读的channel,事实上channel只读或只写都没有意义,所谓的单向channel其实只是声明时用,比如
func foo(ch chan<- int) <-chan int {...}
chan<-int表示一个只可写入的channel,<-chan int表示一个只可读取的cahnel。上面这个函数约定了foo内只能从向ch中写入数据,返回只一个只能读取的channel,虽然使用普通的channel也没有问题,但这样在方法声明时约定可以防止channel被滥用,这种预防机制发生再编译期间。
channel的主要实现在src/runtime/chan.go中,一下源码均基于go 1.9.2。源码阅读时为了更好理解channel特性,帮助正确合理的使用 channel,阅读代码的过程可以回忆前面章节的 channel 特性。
channel相关类定义如下:
// channel类型定义
type hchan struct {
// channel中的元素数量,len
qcount uint // total data in the queue
// channel的大小, cap
dataqsiz uint //size of the circular queue
// channel的缓冲区,环形数组实现
buf unsafe.Pointer // points to an array of dataqsiz elements
// 单个元素的大小
elemsize uint16
// closed 标志位
closed uint32
// 元素的类型
elemtype *_type // element type
// send 和receive的索引,用于实现环形数组队列
sendx uint //send index
recvx uint //receive index
// recv goroutine 等待队列
recvq waitq // list of recv waiters
// send goroutine 等待队列
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
// 等待队列的链表实现
type waitq struct {
first *sudog
last *sudog
}
// in src/runtime/runtime2.go
// 对G的封装
type sudog struct {
// The following fields are protected by the hchan.lock of the
// channel this sudog is blocking on. shrinkstack depends on
// this for sudogs involved in channel ops.
g *g
selectdone *uint32 // CAS to 1 to win select race (may point to stack)
next *sudog
prev *sudog
elem unsafe.Pointer // data element(may point to stack)
// The following fields are never accessed concurrently.
// For channels, waitlink is only accessed by g.
// For semaphores, all fields (including the ones above)
// are only accessed when holding a semaRoot lock.
acquiretime int64
releasetime int64
ticket uint32
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog //semaRoot
c *hchan // channel
}
可以看到,channel的主要组成有:一个环形数组实现的队列,用于存储消息元素;两个链表实现的goroutine等待队列,用于存储阻塞在recv和send操作上的goroutine;一个互斥锁,用于各个属性变动的同步。
func makchan(t *chantype, size int64) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
if size < 0 || int64(uintpr(size)) != size || (elem.size > 0 && uintptr(size) > (_MaxMem-hchanSize)/elem.size) {
panic(plainError("makechan: size out of range"))
}
var c *hchan
if elem.kind&kindNoPointers != 0 || size == 0 {
// case 1: channel 不含有指针
// case 2: size == 0, 即无缓冲 channel
// Allocate memory in one call.
// Hchan does not contain pointers interesting for GC in this case:
// buf pointers into the same allocation, elemtype is persistent.
// SudoG's are referenced from their owning thread so they can't be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
// 在堆上分配连续的空间用作channel
c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
if size > 0 && elem.size != 0 {
c.buf = add(unsafe.Pointer(c), hchanSize)
} else {
// race detector uses this location for synchronization
// Also prevents us from pointing beyond the allocation(see issue 9401).
c.buf = unsafe.Pointer(c)
}
} else {
// 有缓冲channel初始化
c = new(hchan)
// 堆上分配buf内存
c.buf = newarray(elem, int(size))
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsize=", size, "\n")
}
return c
}
make的过程还比较简单,需要注意一点的是当元素不含指针的时候,会将整个hchan分配成一个连续的空间。
// entry point for c <- x from compiled code
// go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, getcallerpc(unsafe.Pointer(&c)))
}
/*
* generic single channel send/recv
* If block is not nil,
* then the protocol will not
* sleep but return if it could
* not complete.
*
* sleep can wake up with g.param == nil
* when a channel involved in the sleep has
* been closed. it is easiest to loop and re-run
* the operation; we'll see that it's now closed.
*/
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 前面章节说到的,当channel未初始化或为nil时,向其中发送数据将会永久阻塞
if c == nil {
if !block {
return false
}
// gopark 会使当前goroutine休眠,并通过unlockf唤醒,但是此时传入的unlockf为nil,因此,goroutine会一直休眠
gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(unsafe.Pointer(c), callerpc, funcPC(chansend))
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
//获取同步锁
lock(&c.lock)
//之前章节提过,向已经关闭的channel发送消息会产生panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// CASE1: 当有goroutine在recv队列上等待时,跳过缓存队列,将消息直接发给receiver goroutine
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// CASE2: 缓存队列未满,则将消息复制到缓存队列上
if c.qcount < c.dataqsiz {
//Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcoun++
unlock(&c.lock)
return true
}
if !block {
unlock(&c.lock)
return false
}
// CASE3: 缓存队列已满,将goroutine加入send队列
// 初始化 sudog
// Block on the channel.Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 加入队列
c.sendq.enqueue(mysg)
// 休眠
goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)
// 唤醒 goroutine
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime >0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
从send代码中可以看到,之前章节提到的一些特性都在代码中有所体现
send有以下几种情况:
//entry points for <- c from compiled code
//go:nosplit
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool)(selected, received bool) {
// raceenabled: don't need to check ep, as it is always on the stack
// or is new memory allocated by reflect.
if debugChan {
print("chanrecv: chan=", c, "\n")
}
// 从nil的channel中接收消息,永久阻塞
if c == nil {
if !block {
return
}
gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not ready for receiving, we observe that the
// channel is not closed. Each of these observations is a single word-sized read
// (first c.sendq.first or c.qcount, and second c.closed).
// Because a channel cannot be reopened, the later observation of the channel
// being not closed implies that it was also not closed at the moment of the
// first observation. We behave as if we observed the channel at that moment
// and report that the receive cannot proceed.
//
// The order of operations is important here: reversing the operations can lead to
// incorrect behavior when racing with a close.
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
return
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
// CASE1: 从已经close且为空的channel recv数据,返回控制
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(unsafe.Pointer(c))
}
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// CASE2: send队列不为空
// CASE2.1: 缓存队列为空,直接从sender recv元素
// CASE2.2: 缓存队列不为空,此时只有可能是缓存队列已满,从队列头取出元素,并唤醒sender将元素写入缓存队列尾部。由于是环形队列,因此,队列满时只需要将队列头赋值给receiver,同时将sender元素复制到该位置,并移动队列头尾索引,不需要移动队列元素
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender, If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queu
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// CASE3: 缓存队列不为空,直接从队列去元素,移动头索引
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
if !block {
unlock(&c.lock)
return false, false
}
// CASE4: 缓存队列为空,将goroutine加入recv队列,并阻塞
// no sender available: block on this channel
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.selectdone = nil
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)
// someone woke us up
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, !closed
}
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
// 重复close,产生panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
if raceenabled {
callerpc := getcallerpc(unsafe.Pointer(&c))
racewritepc(unsafe.Pointer(c), callerpc, funcPC(closechan))
racerelease(unsafe.Pointer(c))
}
c.closed = 1
var glist *g
//唤醒所有 receiver
// release all readers
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, unsafe.Pointer(c))
}
gp.schedlink.set(glist)
glist = gp
}
// 唤醒所有sender,并产生panic
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, unsafe.Pointer(c))
}
gp.schedlink.set(glist)
glist = gp
}
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
for glist != nil {
gp := glist
glist = glist.schedlink.ptr()
gp.schedlink = 0
goready(gp, 3)
}
}