作者:@daemon365
本文为作者原创,转载请注明出处:https://www.cnblogs.com/daemon365/p/18628395
作用
- Go 语言的 channel 是一种 goroutine 之间的通信方式,它可以用来传递数据,也可以用来同步 goroutine 的执行。
- chan 是 goroutine 之间的通信桥梁,可以安全地在多个 goroutine 中共享数据。
- 使用 chan 实现 goroutine 之间的协作与同步,可用于信号传递、任务完成通知等。
- select 配合 chan,可以同时监听多个 channel,处理任意一个可用 channel 的数据。
结构
| type hchan struct { |
| qcount uint |
| dataqsiz uint |
| buf unsafe.Pointer |
| elemsize uint16 |
| closed uint32 |
| timer *timer |
| elemtype *_type |
| sendx uint |
| recvx uint |
| recvq waitq |
| sendq waitq |
| |
| |
| lock mutex |
| } |

waitq
| type waitq struct { |
| first *sudog |
| last *sudog |
| } |
sudog
| type sudog struct { |
| g *g |
| |
| next *sudog |
| prev *sudog |
| elem unsafe.Pointer |
| |
| acquiretime int64 |
| releasetime int64 |
| ticket uint32 |
| |
| isSelect bool |
| |
| success bool |
| |
| waiters uint16 |
| |
| parent *sudog |
| waitlink *sudog |
| waittail *sudog |
| c *hchan |
| } |
创建
创建一个 channel:
| func makechan(t *chantype, size int) *hchan { |
| |
| elem := t.Elem |
| |
| |
| 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")) |
| } |
| |
| var c *hchan |
| switch { |
| case mem == 0: |
| |
| |
| c = (*hchan)(mallocgc(hchanSize, nil, true)) |
| c.buf = c.raceaddr() |
| case !elem.Pointers(): |
| |
| c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) |
| c.buf = add(unsafe.Pointer(c), hchanSize) |
| default: |
| |
| 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 |
| } |
| const hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1)) |
| |
| func (c *hchan) raceaddr() unsafe.Pointer { |
| |
| |
| |
| |
| return unsafe.Pointer(&c.buf) |
| } |
写
| func chansend1(c *hchan, elem unsafe.Pointer) { |
| chansend(c, elem, true, getcallerpc()) |
| } |
| |
| func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { |
| if c == nil { |
| if !block { |
| return false |
| } |
| |
| gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2) |
| throw("unreachable") |
| } |
| |
| |
| |
| |
| if !block && c.closed == 0 && full(c) { |
| return false |
| } |
| |
| var t0 int64 |
| if blockprofilerate > 0 { |
| t0 = cputicks() |
| } |
| |
| lock(&c.lock) |
| |
| if c.closed != 0 { |
| unlock(&c.lock) |
| panic(plainError("send on closed channel")) |
| } |
| |
| if sg := c.recvq.dequeue(); sg != nil { |
| |
| send(c, sg, ep, func() { unlock(&c.lock) }, 3) |
| return true |
| } |
| |
| if c.qcount < c.dataqsiz { |
| |
| qp := chanbuf(c, c.sendx) |
| if raceenabled { |
| racenotify(c, c.sendx, nil) |
| } |
| typedmemmove(c.elemtype, qp, ep) |
| c.sendx++ |
| if c.sendx == c.dataqsiz { |
| c.sendx = 0 |
| } |
| c.qcount++ |
| unlock(&c.lock) |
| return true |
| } |
| |
| if !block { |
| |
| unlock(&c.lock) |
| return false |
| } |
| |
| |
| gp := getg() |
| |
| mysg := acquireSudog() |
| mysg.releasetime = 0 |
| if t0 != 0 { |
| mysg.releasetime = -1 |
| } |
| |
| mysg.elem = ep |
| mysg.waitlink = nil |
| mysg.g = gp |
| mysg.isSelect = false |
| mysg.c = c |
| gp.waiting = mysg |
| gp.param = nil |
| c.sendq.enqueue(mysg) |
| |
| gp.parkingOnChan.Store(true) |
| gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2) |
| |
| KeepAlive(ep) |
| |
| |
| if mysg != gp.waiting { |
| throw("G waiting list is corrupted") |
| } |
| gp.waiting = nil |
| gp.activeStackChans = false |
| closed := !mysg.success |
| gp.param = nil |
| if mysg.releasetime > 0 { |
| blockevent(mysg.releasetime-t0, 2) |
| } |
| mysg.c = nil |
| |
| releaseSudog(mysg) |
| if closed { |
| if c.closed == 0 { |
| throw("chansend: spurious wakeup") |
| } |
| panic(plainError("send on closed channel")) |
| } |
| return true |
| } |
所以阻塞写这个主要有三种模式:
- 如果有等待接收的 Goroutine (c.recvq 里面有值),说明 buf 要么满了 要么就没有,直接将值发送给它,跳过缓冲区
- 如果通道缓冲区有空间,直接将值写入缓冲区
- 如果缓冲区没有空间,且是阻塞模式,当前 Goroutine 挂起等待接收者

send
| 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 |
| } |
| gp := sg.g |
| unlockf() |
| gp.param = unsafe.Pointer(sg) |
| sg.success = true |
| if sg.releasetime != 0 { |
| sg.releasetime = cputicks() |
| } |
| |
| |
| goready(gp, skip+1) |
| } |
| |
| func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) { |
| |
| dst := sg.elem |
| typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_) |
| memmove(dst, src, t.Size_) |
| } |
大致的逻辑为,取出 goroutine 然后把接受的值 COPY 到接受者的内存中,然后唤醒接受者 goroutine。
接受的内存可能是堆也可能是栈,堆还好说,如果是栈,就是在一个栈内直接操作其他的栈了,按理来说,这是不安全的。但是,这是 runtime, 我们已经把 goroutine GoPark 了,保证了它不会执行,所以这里是安全的。当然我们自己写代码时,肯定是不能这么做的。
chanbuf && typedmemmove
| func chanbuf(c *hchan, i uint) unsafe.Pointer { |
| |
| return add(c.buf, uintptr(i)*uintptr(c.elemsize)) |
| } |
| |
| func typedmemmove(typ *abi.Type, dst, src unsafe.Pointer) { |
| if dst == src { |
| return |
| } |
| if writeBarrier.enabled && typ.Pointers() { |
| |
| bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.PtrBytes, typ) |
| } |
| |
| memmove(dst, src, typ.Size_) |
| if goexperiment.CgoCheck2 { |
| cgoCheckMemmove2(typ, dst, src, 0, typ.Size_) |
| } |
| } |
acquireSudog & releaseSudog
| func acquireSudog() *sudog { |
| mp := acquirem() |
| pp := mp.p.ptr() |
| |
| |
| if len(pp.sudogcache) == 0 { |
| lock(&sched.sudoglock) |
| for len(pp.sudogcache) < cap(pp.sudogcache)/2 && sched.sudogcache != nil { |
| s := sched.sudogcache |
| sched.sudogcache = s.next |
| s.next = nil |
| pp.sudogcache = append(pp.sudogcache, s) |
| } |
| unlock(&sched.sudoglock) |
| if len(pp.sudogcache) == 0 { |
| pp.sudogcache = append(pp.sudogcache, new(sudog)) |
| } |
| } |
| |
| |
| n := len(pp.sudogcache) |
| s := pp.sudogcache[n-1] |
| pp.sudogcache[n-1] = nil |
| pp.sudogcache = pp.sudogcache[:n-1] |
| if s.elem != nil { |
| throw("acquireSudog: found s.elem != nil in cache") |
| } |
| releasem(mp) |
| return s |
| } |
| |
| func releaseSudog(s *sudog) { |
| |
| gp := getg() |
| |
| mp := acquirem() |
| pp := mp.p.ptr() |
| if len(pp.sudogcache) == cap(pp.sudogcache) { |
| |
| var first, last *sudog |
| for len(pp.sudogcache) > cap(pp.sudogcache)/2 { |
| n := len(pp.sudogcache) |
| p := pp.sudogcache[n-1] |
| pp.sudogcache[n-1] = nil |
| pp.sudogcache = pp.sudogcache[:n-1] |
| if first == nil { |
| first = p |
| } else { |
| last.next = p |
| } |
| last = p |
| } |
| lock(&sched.sudoglock) |
| last.next = sched.sudogcache |
| sched.sudogcache = first |
| unlock(&sched.sudoglock) |
| } |
| |
| pp.sudogcache = append(pp.sudogcache, s) |
| releasem(mp) |
| } |
读
| 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 |
| } |
| |
| func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { |
| |
| |
| |
| if !block && empty(c) { |
| if atomic.Load(&c.closed) == 0 { |
| |
| return |
| } |
| if empty(c) { |
| if raceenabled { |
| raceacquire(c.raceaddr()) |
| } |
| if ep != nil { |
| typedmemclr(c.elemtype, ep) |
| } |
| return true, false |
| } |
| } |
| |
| var t0 int64 |
| if blockprofilerate > 0 { |
| t0 = cputicks() |
| } |
| |
| lock(&c.lock) |
| |
| if c.closed != 0 { |
| |
| if c.qcount == 0 { |
| if raceenabled { |
| raceacquire(c.raceaddr()) |
| } |
| unlock(&c.lock) |
| if ep != nil { |
| |
| typedmemclr(c.elemtype, ep) |
| } |
| return true, false |
| } |
| } else { |
| |
| if sg := c.sendq.dequeue(); sg != nil { |
| |
| recv(c, sg, ep, func() { unlock(&c.lock) }, 3) |
| return true, true |
| } |
| } |
| |
| |
| if c.qcount > 0 { |
| qp := chanbuf(c, c.recvx) |
| if raceenabled { |
| racenotify(c, c.recvx, nil) |
| } |
| 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 |
| } |
| |
| |
| if !block { |
| unlock(&c.lock) |
| return false, false |
| } |
| |
| |
| gp := getg() |
| mysg := acquireSudog() |
| mysg.releasetime = 0 |
| if t0 != 0 { |
| mysg.releasetime = -1 |
| } |
| mysg.elem = ep |
| mysg.waitlink = nil |
| gp.waiting = mysg |
| |
| mysg.g = gp |
| mysg.isSelect = false |
| mysg.c = c |
| gp.param = nil |
| c.recvq.enqueue(mysg) |
| if c.timer != nil { |
| blockTimerChan(c) |
| } |
| |
| gp.parkingOnChan.Store(true) |
| gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2) |
| |
| |
| if mysg != gp.waiting { |
| throw("G waiting list is corrupted") |
| } |
| if c.timer != nil { |
| unblockTimerChan(c) |
| } |
| gp.waiting = nil |
| gp.activeStackChans = false |
| if mysg.releasetime > 0 { |
| blockevent(mysg.releasetime-t0, 2) |
| } |
| success := mysg.success |
| gp.param = nil |
| mysg.c = nil |
| releaseSudog(mysg) |
| return true, success |
| } |
所以读数据(阻塞读)的逻辑为:
- 检查 channel 是否已经关闭 如果关闭了 而且没有数据了 直接返回
- 如果有等待发送的 Goroutine (c.sendq 里面有值),如果无缓冲chan 直接从goroutine中取值 负责从 buf 取出值 并把数据加入末尾
- 如果缓冲区中有数据,从缓冲区接收
- 如果缓冲区没有数据了 挂起 goroutine 并加入 recvq 等待接收者

recv
| func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { |
| if c.dataqsiz == 0 { |
| |
| if ep != nil { |
| |
| recvDirect(c.elemtype, sg, ep) |
| } |
| } else { |
| |
| |
| qp := chanbuf(c, c.recvx) |
| |
| |
| if ep != nil { |
| typedmemmove(c.elemtype, ep, qp) |
| } |
| |
| typedmemmove(c.elemtype, qp, sg.elem) |
| c.recvx++ |
| if c.recvx == c.dataqsiz { |
| c.recvx = 0 |
| } |
| c.sendx = c.recvx |
| } |
| sg.elem = nil |
| gp := sg.g |
| unlockf() |
| gp.param = unsafe.Pointer(sg) |
| sg.success = true |
| if sg.releasetime != 0 { |
| sg.releasetime = cputicks() |
| } |
| goready(gp, skip+1) |
| } |
| |
关闭
| 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")) |
| } |
| |
| if raceenabled { |
| callerpc := getcallerpc() |
| racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan)) |
| racerelease(c.raceaddr()) |
| } |
| |
| |
| c.closed = 1 |
| |
| |
| var glist gList |
| |
| |
| for { |
| sg := c.recvq.dequeue() |
| if sg == nil { |
| break |
| } |
| if sg.elem != nil { |
| typedmemclr(c.elemtype, sg.elem) |
| sg.elem = nil |
| } |
| if sg.releasetime != 0 { |
| sg.releasetime = cputicks() |
| } |
| gp := sg.g |
| gp.param = unsafe.Pointer(sg) |
| sg.success = false |
| if raceenabled { |
| raceacquireg(gp, c.raceaddr()) |
| } |
| glist.push(gp) |
| } |
| |
| |
| for { |
| sg := c.sendq.dequeue() |
| if sg == nil { |
| break |
| } |
| sg.elem = nil |
| if sg.releasetime != 0 { |
| sg.releasetime = cputicks() |
| } |
| gp := sg.g |
| gp.param = unsafe.Pointer(sg) |
| sg.success = false |
| if raceenabled { |
| raceacquireg(gp, c.raceaddr()) |
| } |
| glist.push(gp) |
| } |
| unlock(&c.lock) |
| |
| |
| for !glist.empty() { |
| gp := glist.pop() |
| gp.schedlink = 0 |
| goready(gp, 3) |
| } |
| } |
非阻塞读写
非阻塞的方式一般用在 select
中。
| func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { |
| return chansend(c, elem, false, getcallerpc()) |
| } |
| |
| func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) { |
| return chanrecv(c, elem, false) |
| } |
- 在阻塞下,需要当前 goroutine 挂起时,非阻塞则不需要,直接返回 flase。
- 如果能直接读数据,则返回 true。
select
| func walkSelectCases(cases []*ir.CommClause) []ir.Node { |
| |
| switch n.Op() { |
| default: |
| base.Fatalf("select %v", n.Op()) |
| |
| case ir.OSEND: |
| |
| n := n.(*ir.SendStmt) |
| ch := n.Chan |
| cond = mkcall1(chanfn("selectnbsend", 2, ch.Type()), types.Types[types.TBOOL], r.PtrInit(), ch, n.Value) |
| |
| case ir.OSELRECV2: |
| n := n.(*ir.AssignListStmt) |
| recv := n.Rhs[0].(*ir.UnaryExpr) |
| ch := recv.X |
| elem := n.Lhs[0] |
| if ir.IsBlank(elem) { |
| elem = typecheck.NodNil() |
| } |
| cond = typecheck.TempAt(base.Pos, ir.CurFunc, types.Types[types.TBOOL]) |
| fn := chanfn("selectnbrecv", 2, ch.Type()) |
| call := mkcall1(fn, fn.Type().ResultsTuple(), r.PtrInit(), elem, ch) |
| as := ir.NewAssignListStmt(r.Pos(), ir.OAS2, []ir.Node{cond, n.Lhs[1]}, []ir.Node{call}) |
| r.PtrInit().Append(typecheck.Stmt(as)) |
| } |
| |
| |
| } |
| func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { |
| return chansend(c, elem, false, getcallerpc()) |
| } |
| |
| func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) { |
| return chanrecv(c, elem, false) |
| } |
改写后就是调用 selectnbsend
非阻塞的从 channel 发送数据,如果成功则返回 true,否则返回 false。失败了就从下个 case 继续执行。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析