【Golang基础】channel

74 阅读3分钟

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结构体的解释

hchan.jpg

由以上字段我们可以知道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 和缓冲区:

  1. 不存在缓冲区(size=0):只给runtime.hchan 分配一段空间, hchan.buf 不创建。
  2. 当channel的储存类型不是指针类型:会为channel和底层数组分配一块连续的内存空间。
  3. 默认情况:会单独为 runtime.hchan 和缓冲区分配内存。

发送数据

Go 在编译发送给chan的时候,会把发送语句转换成 runtime.chansend1 ,再调用 runtime.chansend

这个过程比较复杂,我们简单将这个过程分为三个部分:

  1. 当存在等待的接收者时,通过 runtime.send直接将数据发送给阻塞的接收者。
  2. (没有接收者)当缓冲区存在空余空间时,将发送的数据写入channel的缓冲区。
  3. 当不存在缓冲区或者缓冲区满时,等待其他goroutine 从channel接收数据。

注意:向一个已经关闭的channel发送数据,会painc。

接收数据

在处理从 chan 中接收数据时,Go 会把代码转换成 chanrecv1 函数,如果要返回两个返回值,会转换成 chanrecv2,chanrecv1 函数和 chanrecv2 会调用 runtime.chanrecv

我们也简单将这个过程分为三个部分:

  1. 当存在等待的发送者时,通过 runtime.recv 从阻塞的发送者或者缓冲区中获取数据。
  2. 当缓冲区存在数据时,从 Channel 的缓冲区中接收数据。
  3. 当缓冲区中不存在数据时,等待其他 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)从队列中全部移除并唤醒。