一文搞懂Go通道

1.简介channel是Go语言的一大特性,基于channel有很多值得探讨的问题,如

  1. channel为什么是并发安全的?
  2. 同步通道和异步通道有啥区别?
  3. 通道为何会阻塞协程?
  4. 使用通道导致阻塞的协程是如何解除阻塞的?
要了解本质,需要进源码查看,毕竟源码之下了无秘密 。
2.原理2.1创建channel理论上有三种,带缓冲不带缓冲nil,写法如下:
// bufferedch := make(chan Task, 3)// unbufferedch := make(chan int)// nilvar ch chan int复制代码追踪make函数,会发现在builtin/builtin.go中仅有一个声明func make(t Type, size ...IntegerType) Type 。真正的实现可以参考go内置函数make,简单来说在
cmd/compile/internal/gc/typecheck.go中有函数typecheck1
// The result of typecheck1 MUST be assigned back to n, e.g.//n.Left = typecheck1(n.Left, top)func typecheck1(n *Node, top int) (res *Node) { if enableTrace && trace {defer tracePrint("typecheck1", n)(&res) } switch n.Op { case OMAKE:ok |= ctxExprargs := n.List.Slice()if len(args) == 0 {yyerror("missing argument to make")n.Type = nilreturn n}n.List.Set(nil)l := args[0]l = typecheck(l, Etype)t := l.Typeif t == nil {n.Type = nilreturn n}i := 1switch t.Etype {default:yyerror("cannot make type %v", t)n.Type = nilreturn ncase TCHAN:l = nilif i < len(args) {l = args[i]i++l = typecheck(l, ctxExpr)l = defaultlit(l, types.Types[TINT])if l.Type == nil {n.Type = nilreturn n}if !checkmake(t, "buffer", l) {n.Type = nilreturn n}n.Left = l} else {n.Left = nodintconst(0)}n.Op = OMAKECHAN //对应的函数位置}if i < len(args) {yyerror("too many arguments to make(%v)", t)n.Op = OMAKEn.Type = nilreturn n}n.Type = tif (top&ctxStmt != 0) && top&(ctxCallee|ctxExpr|Etype) == 0 && ok&ctxStmt == 0 {if !n.Diag() {yyerror("%v evaluated but not used", n)n.SetDiag(true)}n.Type = nilreturn n}return n }}复制代码最终真正实现位置为runtime/chan.go
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 *hchanswitch {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.kind&kindNoPointers != 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 = elemc.dataqsiz = uint(size)if debugChan {print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "n")}return c}复制代码从这个函数可以看出,channel的数据结构为hchan
2.2结构接下来我们看一下channel的数据结构,基于数据结构,可以推测出具体实现 。
runtime/chan.go
type hchan struct { //channel队列里面总的数据量 qcountuint// total data in the queue // 循环队列的容量,如果是非缓冲的channel就是0 dataqsiz uint// size of the circular queue // 缓冲队列,数组类型 。bufunsafe.Pointer // points to an array of dataqsiz elements // 元素占用字节的size elemsize uint16 // 当前队列关闭标志位,非零表示关闭 closeduint32 // 队列里面元素类型 elemtype *_type // element type // 队列send索引 sendxuint// send index // 队列索引 recvxuint// receive index // 等待channel的G队列 。recvqwaitq// list of recv waiters // 向channel发送数据的G队列 。sendqwaitq// 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}复制代码通过该hchan的数据结构和makechan函数,数据结构里有几个值得说明的数据:
  1. dataqsiz表示channel的长度,如果为非缓冲队列,则值为0 。通过dataqsiz实现环形队列 。


    推荐阅读