golang里的channel信道是golang里一个独特的概念,基于消息通信的方式来实现并发控制。信道有两种类型,缓存型和非缓存型,其中缓冲型底层基于循环数组来保存数据,然后基于互斥锁保证并发访问安全。对于信道可以有三种操作,分别是读,写以及关闭。读一个nil信道,当前协程会被挂起,读一个已经关闭的信道,如果信道中有元素可以正常读取,如果没有会读取对应类型的零值。如果信道是非缓冲的,等待发送队列中有协程,直接从对应协程中拷贝数据,否则或者如果缓冲为空当前协程阻塞,加入到信道的等待发送队列中。则从缓冲头部中读取一个数据,写一个nil信道,同样会被挂起,写一个已经关闭的信道,会panic,写的时候,也是类似,如果信道的等待接收队列中有协程,直接将数据拷贝过去,否则将当前协程阻塞,加入到等待发送队列。如果关闭一个nil或者已经关闭的信道,也会panic。
本文将从源码分析的角度验证以上的观点。最后实现golang的信道来实现一个多线程打印问题。
源码分析
channel相关的代码在runtime包下的chan.go文件中。
结构体定义和构造函数
首先关注信道的结构体定义,源码如下:
type hchan struct {
qcount uint // total data in the queue // 数量数量
dataqsiz uint // size of the circular queue // 循环队列的长度
buf unsafe.Pointer // points to an array of dataqsiz elements // 实现循环队列的底层数组的起始地址
elemsize uint16 // 每一个元素的大小
closed uint32 // 是否关闭的标志位
elemtype *_type // element type
sendx uint // send index // 队头指针,指向要发送的数据的位置
recvx uint // receive index // 队尾指针,指向可以存放数据的位置
recvq waitq // list of recv waiters // 因为从信道接受而阻塞的协程的链表
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 // 并发访问的互斥锁
}
发送等待或者接收等待的waitq是一个链表,链表上面的每一个节点是一个指向包装go协程的sudog,其结构体定义如下:
type waitq struct {
// 用于保存阻塞在信道上的协程的双向链表
first *sudog
last *sudog
}
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 // 指向被阻塞的协程
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
// isSelect indicates g is participating in a select, so
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
// success indicates whether communication over channel c
// succeeded. It is true if the goroutine was awoken because a
// value was delivered over channel c, and false if awoken
// because c was closed.
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
最后来看一下信道的构造函数,源代码如下:
func makechan(t *chantype, size int) *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")
}
mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points 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.
var c *hchan
switch {
case mem == 0:
// Queue or element size is zero.
// 无缓冲或者元素大小为零分配的96个字节
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.PtrBytes == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
// 保存的元素不含有指针
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.Size_)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")
}
return c
}
发送操作
所有相关的源码如下:
// chansend 为通用的信道发送函数,实际上我们使用的 c <- x, 经过编译调用的是chansend1函数,其又会调用chansend函数,而传入的函数block是true,也就是说要阻塞,但这个调用的信道发送函数可以实现当无法发送的时候可以不阻塞
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
// 信道为nil
if !block {
// 不阻塞,返回
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2) // 阻塞模式下,panic
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(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 full()).
// 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. However, nothing here
// guarantees forward progress. We rely on the side effects of lock release in
// chanrecv() and closechan() to update this thread's view of c.closed and full().
// 非阻塞模式下,且信道未关闭并且无法发送数据(缓冲队列已满或者等待接收队列为空),此时直接返回false,走捷径返回,避免加锁的开销
if !block && c.closed == 0 && fu

最低0.47元/天 解锁文章
1063

被折叠的 条评论
为什么被折叠?



