不要用共享内存的方式通信,而要用通信的方式共享内存
channel 是 go 中的一等公民
通过共享内存来通信,会导致数据竞争,从而导致程序出现不可预知的错误
func watch (p *int) {
for {
if *p == 1 {
fmt.Println(*p)
break
}
}
}
func main() {}{
i := 0
go watch(&i)
time.Sleep(1 * time.Second)
i = 1
}
通过通信的方式共享内存,可以避免数据竞争
func watch (c chan int) {
if <-c == 1 {
fmt.Println(*p)
}
}
func main() {
c := make(chan int)
go watch(c)
time.Sleep(time.Second)
c <- 1
}
如何设计 Channel
channel 由三部分组成:
- 缓存区
- 发送等待队列
- 接收等待队列

channel 在 go 中本质是一个 hchan 的结构体,定义在 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
}
channel 的缓存区由 qcount、dataqsiz、buf、elemsize、elemtype 这 5 个字段组成的环形缓存区(ring buffer)
buf:是一个指针,指向ring buffer的起始地址elemsize:是一个uint16类型的字段,表示元素的大小elemtype:是一个指针,指向元素的类型qcount:是一个uint类型的字段,表示缓存区中的元素个数
环形缓存区的好处是可以循环使用数据,相比于队列的优点是大幅降低了 GC 的开销
发送队列由:
sendx:是一个uint类型的字段,表示发送的索引sendq:是一个waitq类型的字段,表示发送等待队列
接收队列由:
recvx:是一个uint类型的字段,表示接收的索引recvq:是一个waitq类型的字段,表示接收等待队列
waitq 是一个链表的结构体,定义在 runtime/chan.go 中,first 指向链表的第一个成员,last 指向链表的最后一个成员,它们都是 sudog 类型的指针
type waitq struct {
first *sudog
last *sudog
}
hchan 结构体还有一个 lock 字段,是一个 mutex 类型的字段,用来保护 hchan 结构体的所有字段
所以 channel 本身不是无锁的
那为什么 channel 的并发量还是很大呢?是因为只有在存数据或者取数据的时候才会加锁,操作完之后立马就会释放锁,所以 channel 的并发量会很大
closed:是一个uint32类型的字段,表示channel是否关闭,0表示开始,1表示关闭
channel 发送数据的底层原理
发送数据在 go 中表示 c <- 1,它的底层是调用 runtime.chansend1 函数
chansend1 函数定义在 runtime/chan.go 中,它的作用是向 channel 中发送数据
chansend1 函数调用 chansend 函数
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
channel 发送数据分为三种情况:
- 直接发送
- 放入缓存
- 休眠等待
直接发送
在数据发送之前,已经有协程在等待接收数据
这个时候缓存中是没有数据,因为如果有数据的话就不会等待了
协程会放入 recvq 队列中等待,等待数据过来被唤醒
数据过来后,直接唤醒 recvq 队列中的协程,然后将数据发送给它,不需要放入缓存区
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 从 recvq 队列中取一个等待的队列
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
}
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
// 唤醒沉睡的协程
goready(gp, skip+1)
}
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
dst := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
// No need for cgo write barrier checks because dst is always
// Go memory.
// 取了一个目的地的指针 sg.elem,就是 i,i := <-c
// 直接将要发送的数据移动给了要接收的那个变量
memmove(dst, src, t.Size_)
}
放入缓存
方法缓存的意思是没有协程在等待接收数据,但是缓存区中有空间可以放数据
放入缓存的逻辑不需要和其他协程有交集,直接放入缓存区即可
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 缓存区最多能缓存多少
// 从环形缓存区拿出一个可以用的缓存
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 将要发送的数据放到可以用的缓存区中
typedmemmove(c.elemtype, qp, ep)
// 索引+1
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 增加缓存区中的元素个数
c.qcount++
unlock(&c.lock)
return true
}
}
休眠等待
休眠等待的意思是没有协程在等待接收数据,缓存区中也没有空间可以放数据
这个时候就需要把当前协程包装成 sudog 放入 sendq 队列中等待,等待有协程来接收数据
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 拿到自己的结构体
gp := getg()
// 把自己包装成 sudog
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.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 把自己放到 sendq 队列中
c.sendq.enqueue(mysg)
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
gp.parkingOnChan.Store(true)
// 休眠
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
}
channel 接收数据的底层原理
接收数据在 go 中有两种表示方式:
i <-c,它的底层是调用runtime.chanrecv1函数i, ok <-c,它的底层是调用runtime.chanrecv2函数
chanrecv1 和 chanrecv2 函数最终都会调用 chanrecv 函数
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
channel 接收数据分为四种情况
- 有等待的协程,从协程接收
- 有等待的协程,从缓存接收
- 接收缓存
- 阻塞接收
有等待的协程,从协程接收
在接收数据之前,已经有协程在等待发送数据,而且这个 channel 没有缓存
这时候直接从 send queue 中唤醒一个协程,并将数据拷贝给这个协程
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 从 sendq 队列中取一个等待的协程
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 queue
// 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
}
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 判断缓存区是否为空
if c.dataqsiz == 0 {
if ep != nil {
// copy data from sender
recvDirect(c.elemtype, sg, ep)
}
}
// 唤醒休眠的协程
goready(gp, skip+1)
}
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
// dst is on our stack or the heap, src is on another stack.
// The channel is locked, so src will not move during this
// operation.
src := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
// 直接将要发送的数据移动给了要接收的那个变量
memmove(dst, src, t.Size_)
}
有等待的协程,从缓存接收
在接收数据前,已经有协程在休眠等待,并且缓存区由缓存
从缓存区中取出一个缓存,然后从 send queue 中唤醒一个协程,并将数据拷贝给这个协程
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 从 sendq 队列中取一个等待的协程
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 queue
// 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
}
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// Queue is full. Take the item at the
// head of the queue. Make the sender enqueue
// its item at the tail of the queue. Since the
// queue is full, those are both the same slot.
// 从缓存区取出一个数据
qp := chanbuf(c, c.recvx)
// copy data from queue to receiver
// 将缓存区中的数据拷贝给接收者
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
// 将发送者的数据拷贝给缓存区
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
goready(gp, skip+1)
}
接收缓存
Receive queue 中没有等待的协程,但是缓存区中有数据,直接从缓存中取出数据
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
// 从缓存中取出一个数据,然后拷贝给接收者
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
}
}
阻塞接收
没有协程在休眠等待,而且缓存区中没有缓存,这时候就需要把当前协程包装成 sudog 放入 recvq 队列中等待,等待有协程来发送数据
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 拿到自己的结构体
gp := getg()
// 把自己包装成 sudog
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.isSelect = false
mysg.c = c
gp.param = nil
// 把自己放到 recvq 队列中
c.recvq.enqueue(mysg)
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
gp.parkingOnChan.Store(true)
// 休眠
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)
}