前言
GO官方博客的Share Memory By Communicating中提到:
Go’s concurrency primitives - goroutines and channels - provide an elegant and distinct means of structuring concurrent software. (These concepts have an interesting history that begins with C. A. R. Hoare’s Communicating Sequential Processes.) Instead of explicitly using locks to mediate access to shared data,
Go encourages the use of channels to pass references to data between goroutines
. This approach ensures that only one goroutine has access to the data at a given time. The concept is summarized in the document Effective Go (a must-read for any Go programmer):
Do not communicate by sharing memory; instead, share memory by communicating.
Go原生提供goroutine
及channel
以优雅构建并发软件,并且鼓励通过channel
来传递数据的引用。然后提到了堪称Go并发的准则:
不要通过共享内存来通信,相反,要通过通信来共享内存。
此口号在effective go Concurrency亦有提及。
这个通信的工具自然就是channel
,由此可见channel
的重要性。
关于channel
的介绍明将通过一个小系列的文章说明,本文主要介绍channel的结构及make、close的处理。
更多内容分享,欢迎关注公众号:Go开发笔记
channel
channel
的底层实现位于runtime/chan.go
文件中,以下是官方在文件开头处的注释说明。
// This file contains the implementation of Go channels.
// Invariants:
// At least one of c.sendq and c.recvq is empty,
// except for the case of an unbuffered channel with a single goroutine
// blocked on it for both sending and receiving using a select statement,
// in which case the length of c.sendq and c.recvq is limited only by the
// size of the select statement.
//
// For buffered channels, also:
// c.qcount > 0 implies that c.recvq is empty.
// c.qcount < c.dataqsiz implies that c.sendq is empty.
翻译过来,大致意思是:
此文件包含Go channel的实现。
不变量:
c.sendq(发送等待队列)和c.recvq(接收等待队列)中至少有一个是空的,除了使用select语句为发送和接收而阻塞了单个goroutine的无缓存通道外,在这种情况下,c.sendq和c.recvq的长度仅受select语句大小的限制。
对于缓存通道,也包括:
c.qcount>0表示接收等待队列c.recvq为空。
c.qcount<c.dataqsiz表示发送等待队列c.sendq为空。
关于c.sendq(发送等待队列)和c.recvq(接收等待队列)中至于有一个是空的
,可以这么理解:
为保证chan内数据的队列性(FIFO)
-
发送
时,优先级为:直接发送>缓存
(有等待的接收者时,优先直接发送至等待的接收者,否则缓存,缓存满后则加入发送等待队列) -
接收
时,优先级为:缓存>直接接收
(有缓存时,优先接收缓存,否则若有等待的发送者则直接从等待的发送者接收数据,否则加入接收等待队列)
因此,当有缓存时,说明已没有等待的接收者,接收等待队列为空;而当缓存未满时,数据会继续存在缓存中,而不会加入发送等待队列,发送等待队列为空。
简易的处理机制可以绘制如下图所示:
channel的内部结构
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
底层是一个struct结构的hchan
,其内部参数大致解释如下。
- dataqsiz:
channel
缓存容量,能容纳的最大缓存。 qcount
:当前缓存大小。- 初始化时为0。
- 无缓存的
channel
的qcount
一直为0。 - 对于有缓存的
channel
,当没有等待的接收者时,qcount
增加,qcount
最大为dataqsiz
;没有等待的发送者时,qcount
减少,最小为0。
- elemtype:
channel
类型 - elemsize:
channel
类型的elem的大小 - buf: 指向缓存数组
- sendx: 待发送缓存的索引,用以存入buf
- recvx: 待接收缓存的索引,用以从buf中取出缓存
- sendq:发送等待队列,当发送数据没有接收者接收,且缓存不足时,会加入发送等待队列
- recvq:接收等待队列,当接收者没有数据接收,会加入发送等待队列
- closed: 1为关闭,0为未关闭
- lock: 内部锁
makechan
channel
的make方式如下:
make(chan typ,size)
其内部实现为makechan
:
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.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 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
}
对于数据结构的初始化最终都是对内存地址的分配,channel
的make
过程亦是如此。
- 对chantype的elem的size及align进行校验
- 根据elem的size及数量计算存储这些elem的内存大小,校验是否内存溢出
- 分配内存,构造*hchan
- 对于
无缓存
的channel
,缓存占用的内存为0,直接分配chan自身的大小内存,buf则取自身地址。 - 对于类型中不包含指针的
channel
,一起分配内存,直接分配chan自身的大小+缓存大小的内存,buf指向chan后的缓存地址。 - 对于类型中包含指针的
channel
,分开分配内存,先newchannel
,然后单独分配缓存的内存。
- 对于
- 初始化hchan的参数,
chane
类型大小的elemsize,类型的elemtype,缓存队列大小的dataqsiz。
需要注意的是:
channel
的缓存大小是有限制的,最大为65536channel
分配内存也是有限制的
总结
本文主要重点如下:
-
channel
的基本机制c.sendq(发送等待队列)和c.recvq(接收等待队列)中至于有一个是空的
。-
发送
时,优先级为:直接发送>缓存
(有等待的接收者时,优先直接发送至等待的接收者,否则缓存,缓存满后则加入发送等待队列) -
接收
时,优先级为:缓存>直接接收
(有缓存时,优先接收缓存,否则若有等待的发送者则直接从等待的发送者接收数据,否则加入接收等待队列)
-
-
hchan
是channel
的底层结构 -
make
过程是内存的计算、分配过程,并初始化了channel
的类型、缓存容量参数。
下一篇文章将会关于channel
的核心,发送与接收。