Don’t communicate by sharing memory, share memory by communicating. 不要通过共享内存的方式进行通信,而是应该通过通信的方式共享内存
channel的实现原理
Channel 在运行时使用 runtime.hchan 结构体表示
type hchan struct {
qcount uint
dataqsiz uint
buf unsafe.Pointer
elemsize uint16
closed uint32
elemtype *_type
sendx uint
recvx uint
recvq waitq
sendq waitq
lock mutex
}
下图是对 runtime.hchan结构体的解释
由以上字段我们可以知道channel有一个循环队列,两个等待队列,并且是并发安全的。
初始化
在使用make关键字来创建channel时,底层逻辑会调用 makechan 。
func makechan(t *chantype, size int) *hchan {
elem := t.elem
...
var c *hchan
switch {
case mem == 0:
// chan的size或者元素的size是0,不必创建buf
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素不是指针,分配一块连续的内存给hchan数据结构和buf
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
//hchan数据结构后面紧接着就是buf
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
//元素包含指针,那么单独分配buf
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)
...
return c
}
上述代码根据channel中元素类型和缓冲区大小初始化 runtime.hchan 和缓冲区:
- 不存在缓冲区(size=0):只给
runtime.hchan分配一段空间,hchan.buf不创建。 - 当channel的储存类型不是指针类型:会为channel和底层数组分配一块连续的内存空间。
- 默认情况:会单独为
runtime.hchan和缓冲区分配内存。
发送数据
Go 在编译发送给chan的时候,会把发送语句转换成 runtime.chansend1 ,再调用 runtime.chansend
这个过程比较复杂,我们简单将这个过程分为三个部分:
- 当存在等待的接收者时,通过
runtime.send直接将数据发送给阻塞的接收者。 - (没有接收者)当缓冲区存在空余空间时,将发送的数据写入channel的缓冲区。
- 当不存在缓冲区或者缓冲区满时,等待其他goroutine 从channel接收数据。
注意:向一个已经关闭的channel发送数据,会painc。
接收数据
在处理从 chan 中接收数据时,Go 会把代码转换成 chanrecv1 函数,如果要返回两个返回值,会转换成 chanrecv2,chanrecv1 函数和 chanrecv2 会调用 runtime.chanrecv。
我们也简单将这个过程分为三个部分:
- 当存在等待的发送者时,通过
runtime.recv从阻塞的发送者或者缓冲区中获取数据。 - 当缓冲区存在数据时,从 Channel 的缓冲区中接收数据。
- 当缓冲区中不存在数据时,等待其他 Goroutine 向 Channel 发送数据。
注意:从被关闭并且缓冲区中不存在任何数据的channel接收数据,会获得空值。
关闭通道
通过 close 函数,可以把 chan 关闭,编译器会替换成 runtime.closechan 方法的调用。
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
...
c.closed = 1
var glist gList
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
...
gp := sg.g
...
glist.push(gp)
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
...
gp := sg.g
...
glist.push(gp)
}
unlock(&c.lock)
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
关闭channel的逻辑为:如果 chan 为 nil,close 会 panic;如果 chan 已经 closed,再次 close 也会 panic。否则的话,如果 chan 不为 nil,chan 也没有closed,就把等待队列中的 sender(writer)和 receiver(reader)从队列中全部移除并唤醒。