Golang语言系列-Channel

Golang语言系列-Channel

  • 源码分析
    • 结构体定义和构造函数
    • 发送操作
    • 接受操作
    • 关闭操作
    • select 操作
  • 实验
  • 参考

golang里的channel信道是golang里一个独特的概念,基于消息通信的方式来实现并发控制。信道有两种类型,缓存型和非缓存型,其中缓冲型底层基于循环数组来保存数据,然后基于互斥锁保证并发访问安全。对于信道可以有三种操作,分别是读,写以及关闭。读一个nil信道,当前协程会被挂起,读一个已经关闭的信道,如果信道中有元素可以正常读取,如果没有会读取对应类型的零值。如果信道是非缓冲的,等待发送队列中有协程,直接从对应协程中拷贝数据,否则或者如果缓冲为空当前协程阻塞,加入到信道的等待发送队列中。则从缓冲头部中读取一个数据,写一个nil信道,同样会被挂起,写一个已经关闭的信道,会panic,写的时候,也是类似,如果信道的等待接收队列中有协程,直接将数据拷贝过去,否则将当前协程阻塞,加入到等待发送队列。如果关闭一个nil或者已经关闭的信道,也会panic。
本文将从源码分析的角度验证以上的观点。最后实现golang的信道来实现一个多线程打印问题。

源码分析

channel相关的代码在runtime包下的chan.go文件中。

结构体定义和构造函数

首先关注信道的结构体定义,源码如下:

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 typesendx    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    // 并发访问的互斥锁
}

发送等待或者接收等待的waitq是一个链表,链表上面的每一个节点是一个指向包装go协程的sudog,其结构体定义如下:

type waitq struct {   // 用于保存阻塞在信道上的协程的双向链表first *sudoglast  *sudog
}
type sudog struct {// The following fields are protected by the hchan.lock of the// channel this sudog is blocking on. shrinkstack depends on// this for sudogs involved in channel ops.g *g   // 指向被阻塞的协程next *sudog  // 链表上的下一个prev *sudog   // 链表上的上一个elem unsafe.Pointer // data element (may point to stack)// The following fields are never accessed concurrently.// For channels, waitlink is only accessed by g.// For semaphores, all fields (including the ones above)// are only accessed when holding a semaRoot lock.acquiretime int64releasetime int64ticket      uint32// isSelect indicates g is participating in a select, so// g.selectDone must be CAS'd to win the wake-up race.isSelect bool// success indicates whether communication over channel c// succeeded. It is true if the goroutine was awoken because a// value was delivered over channel c, and false if awoken// because c was closed.success boolparent   *sudog // semaRoot binary treewaitlink *sudog // g.waiting list or semaRootwaittail *sudog // semaRootc        *hchan // channel
}

最后来看一下信道的构造函数,源代码如下:

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.// 无缓冲或者元素大小为零分配的96个字节c = (*hchan)(mallocgc(hchanSize, nil, true))// Race detector uses this location for synchronization.c.buf = c.raceaddr()case elem.PtrBytes == 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)lockInit(&c.lock, lockRankHchan)if debugChan {print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")}return c
}

发送操作

所有相关的源码如下:

// chansend 为通用的信道发送函数,实际上我们使用的 c <- x, 经过编译调用的是chansend1函数,其又会调用chansend函数,而传入的函数block是true,也就是说要阻塞,但这个调用的信道发送函数可以实现当无法发送的时候可以不阻塞 
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {if c == nil {   // 信道为nilif !block {   // 不阻塞,返回return false}gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)   // 阻塞模式下,panicthrow("unreachable")}if debugChan {print("chansend: chan=", c, "\n")}if raceenabled {racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))}// Fast path: check for failed non-blocking operation without acquiring the lock.//// After observing that the channel is not closed, we observe that the channel is// not ready for sending. Each of these observations is a single word-sized read// (first c.closed and second full()).// Because a closed channel cannot transition from 'ready for sending' to// 'not ready for sending', even if the channel is closed between the two observations,// they imply a moment between the two when the channel was both not yet closed// and not ready for sending. We behave as if we observed the channel at that moment,// and report that the send cannot proceed.//// It is okay if the reads are reordered here: if we observe that the channel is not// ready for sending and then observe that it is not closed, that implies that the// channel wasn't closed during the first observation. However, nothing here// guarantees forward progress. We rely on the side effects of lock release in// chanrecv() and closechan() to update this thread's view of c.closed and full().// 非阻塞模式下,且信道未关闭并且无法发送数据(缓冲队列已满或者等待接收队列为空),此时直接返回false,走捷径返回,避免加锁的开销if !block && c.closed == 0 && full(c) {return false}var t0 int64if 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 {// Found a waiting receiver. We pass the value we want to send// directly to the receiver, bypassing the channel buffer (if any).send(c, sg, ep, func() { unlock(&c.lock) }, 3)return true}if c.qcount < c.dataqsiz {   // 信道未满,则将数据拷贝到循环队列的对应位置// Space is available in the channel buffer. Enqueue the element to send.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 {  // 非阻塞模式,则释放锁直接返回falseunlock(&c.lock)return false}// Block on the channel. Some receiver will complete our operation for us.// 阻塞在这一个信道上,加入等待发送队列gp := getg()mysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}// No stack splits between assigning elem and enqueuing mysg// on gp.waiting where copystack can find it.mysg.elem = epmysg.waitlink = nilmysg.g = gpmysg.isSelect = falsemysg.c = cgp.waiting = mysggp.param = nilc.sendq.enqueue(mysg)// Signal to anyone trying to shrink our stack that we're about// to park on a channel. The window between when this G's status// changes and when we set gp.activeStackChans is not safe for// stack shrinking.gp.parkingOnChan.Store(true)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)   // 阻塞自身// Ensure the value being sent is kept alive until the// receiver copies it out. The sudog has a pointer to the// stack object, but sudogs aren't considered as roots of the// stack tracer.KeepAlive(ep)  // 是ep待发送的数据存活,避免被垃圾回收// someone woke us up.// 被唤醒了if mysg != gp.waiting {throw("G waiting list is corrupted")}gp.waiting = nilgp.activeStackChans = falseclosed := !mysg.successgp.param = nilif mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}mysg.c = nilreleaseSudog(mysg)   // 从等待队列中取出自身if closed {if c.closed == 0 {throw("chansend: spurious wakeup")}// 此时信道已经被关闭,panicpanic(plainError("send on closed channel"))}return true
}// send processes a send operation on an empty channel c.
// The value ep sent by the sender is copied to the receiver sg.
// The receiver is then woken up to go on its merry way.
// Channel c must be empty and locked.  send unlocks c with unlockf.
// sg must already be dequeued from c.
// ep must be non-nil and point to the heap or the caller's stack.
// send操作一定是一个空队列c, sg是一个阻塞等待接收的协程,ep是本次要发送到信道中的数据
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if raceenabled {if c.dataqsiz == 0 {racesync(c, sg)} else {// Pretend we go through the buffer, even though// we copy directly. Note that we need to increment// the head/tail locations only when raceenabled.racenotify(c, c.recvx, nil)racenotify(c, c.recvx, sg)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0   // 循环队列}// 因为发送的数据会直接拷贝到等待接受协程,所以循环队列还是空的,这里只是出于某种原因要更新这些位置信息,假装数据先到了信道,然后一个等待协程被唤醒,然后取走了数据c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz}}if sg.elem != nil {sendDirect(c.elemtype, sg, ep)sg.elem = nil}gp := sg.gunlockf()   // 释放本协程持有的对于信道的锁gp.param = unsafe.Pointer(sg)sg.success = trueif sg.releasetime != 0 {sg.releasetime = cputicks()}goready(gp, skip+1)   // 唤醒等待接收协程
}// Sends and receives on unbuffered or empty-buffered channels are the
// only operations where one running goroutine writes to the stack of
// another running goroutine. The GC assumes that stack writes only
// happen when the goroutine is running and are only done by that
// goroutine. Using a write barrier is sufficient to make up for
// violating that assumption, but the write barrier has to work.
// typedmemmove will call bulkBarrierPreWrite, but the target bytes
// are not in the heap, so that will not help. We arrange to call
// memmove and typeBitsBulkBarrier instead.
// sendDirect 直接拷贝数据到等待协程的栈上
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {// src is on our stack, dst is a slot on another stack.// Once we read sg.elem out of sg, it will no longer// be updated if the destination's stack gets copied (shrunk).// So make sure that no preemption points can happen between read & use.dst := sg.elemtypeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)// No need for cgo write barrier checks because dst is always// Go memory.memmove(dst, src, t.Size_)
}

总结一下信道发送的过程:

  1. 首先是检查信道是否为nil(没有初始化),如果为nil,如果是非阻塞模式,则直接返回false,否则panic(用户使用的信道发送都是阻塞的)
  2. 如果处于非阻塞模式下,此时信道已满(无缓冲信道的已满值得是没有等待接收协程队列),则直接返回false表示发送失败,这样做的好处是避免加锁的开销,尽可能减少加锁范围内的代码量
  3. 加锁,再次检查信道是否已经被关闭,如果被关闭则直接panic
  4. 如果等待接收协程队列非空,说明此时信道的状态是空的,此时会直接将数据拷贝给一个等待协程,并且将其唤醒,自身释放锁返回
  5. 如果判断信道状没有满(有缓冲),则将对应数据拷贝到循环队列中,释放锁返回
  6. 如果已满,如果是非阻塞模式,则直接返回false,否则将自身状态更新为阻塞,加入该信道的等待发送队列中
  7. 接下来的代码执行的话,也就是说被唤醒了,此时检查,被唤醒一般有两种情况:一、有一个读信道的协程已经把该阻塞协程的数据拷贝到循环队列了, 二、信道被关闭的时候,会唤醒所有等待发送协程,如果是第一种情况,直接返回,否则也要panic

接受操作

相关源码如下所示

// empty reports whether a read from c would block (that is, the channel is
// empty).  It uses a single atomic read of mutable state.
func empty(c *hchan) bool {// c.dataqsiz is immutable.if c.dataqsiz == 0 {return atomic.Loadp(unsafe.Pointer(&c.sendq.first)) == nil}return atomic.Loaduint(&c.qcount) == 0
}// entry points for <- c from compiled code.
//
//go:nosplit
// 以下两个函数,分别对应有无comma,由编译器根据代码选择一个函数
func chanrecv1(c *hchan, elem unsafe.Pointer) {chanrecv(c, elem, true)
}//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {_, received = chanrecv(c, elem, true)return
}// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
// ep是传入的指针,如果为nil说明调用方不关心取出的是什么值,用户使用的默认是阻塞模式,返回的第一个参数用来实现select,第二个参数表示是否收到的是真值
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {// raceenabled: don't need to check ep, as it is always on the stack// or is new memory allocated by reflect.if debugChan {print("chanrecv: chan=", c, "\n")}if c == nil {if !block {   // 阻塞模式下,直接返回return}gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2)throw("unreachable")  // panic}// Fast path: check for failed non-blocking operation without acquiring the lock.if !block && empty(c) {   // 非阻塞模式下,且信道为空(无缓冲下为空的含义为无等待发送队列)// After observing that the channel is not ready for receiving, we observe whether the// channel is closed.//// Reordering of these checks could lead to incorrect behavior when racing with a close.// For example, if the channel was open and not empty, was closed, and then drained,// reordered reads could incorrectly indicate "open and empty". To prevent reordering,// we use atomic loads for both checks, and rely on emptying and closing to happen in// separate critical sections under the same lock.  This assumption fails when closing// an unbuffered channel with a blocked send, but that is an error condition anyway.if atomic.Load(&c.closed) == 0 {   // 信道被关闭// Because a channel cannot be reopened, the later observation of the channel// being not closed implies that it was also not closed at the moment of the// first observation. We behave as if we observed the channel at that moment// and report that the receive cannot proceed.return}// The channel is irreversibly closed. Re-check whether the channel has any pending data// to receive, which could have arrived between the empty and closed checks above.// Sequential consistency is also required here, when racing with such a send.if empty(c) {   // 还是空的// The channel is irreversibly closed and empty.if raceenabled {raceacquire(c.raceaddr())}if ep != nil {typedmemclr(c.elemtype, ep)}return true, false}}var t0 int64if 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}// The channel has been closed, but the channel's buffer have data.} else {   // 没有被关闭,则如果存在阻塞的等待发送协程,直接进行拷贝数据// Just found waiting sender with not closed.if sg := c.sendq.dequeue(); sg != nil {// Found a waiting sender. If buffer is size 0, receive value// directly from sender. Otherwise, receive from head of queue// and add sender's value to the tail of the queue (both map to// the same buffer slot because the queue is full).recv(c, sg, ep, func() { unlock(&c.lock) }, 3)return true, true}}if c.qcount > 0 {   // 有数据,注意:此时信道也可能处于关闭状态// Receive directly from queueqp := 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}// 到了这里,说明信道为空,非阻塞模式直接返回,阻塞模式需要waitingif !block {   unlock(&c.lock)return false, false}// no sender available: block on this channel.gp := getg()mysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}// No stack splits between assigning elem and enqueuing mysg// on gp.waiting where copystack can find it.mysg.elem = epmysg.waitlink = nilgp.waiting = mysgmysg.g = gpmysg.isSelect = falsemysg.c = cgp.param = nilc.recvq.enqueue(mysg)   // 加入等待接收协程队列// Signal to anyone trying to shrink our stack that we're about// to park on a channel. The window between when this G's status// changes and when we set gp.activeStackChans is not safe for// stack shrinking.gp.parkingOnChan.Store(true)gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)   // 阻塞自身// 被唤醒// someone woke us upif mysg != gp.waiting {throw("G waiting list is corrupted")}gp.waiting = nilgp.activeStackChans = falseif mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}success := mysg.successgp.param = nilmysg.c = nilreleaseSudog(mysg)return true, success
}// recv processes a receive operation on a full channel c.
// There are 2 parts:
//  1. The value sent by the sender sg is put into the channel
//     and the sender is woken up to go on its merry way.
//  2. The value received by the receiver (the current G) is
//     written to ep.
//
// For synchronous channels, both values are the same.
// For asynchronous channels, the receiver gets its data from
// the channel buffer and the sender's data is put in the
// channel buffer.
// Channel c must be full and locked. recv unlocks c with unlockf.
// sg must already be dequeued from c.
// A non-nil ep must point to the heap or the caller's stack.
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {if c.dataqsiz == 0 {   // 无缓冲队列(同步队列),直接将等待发送协程的数据拷贝过来if raceenabled {racesync(c, sg)}if ep != nil {// copy data from senderrecvDirect(c.elemtype, sg, ep)}} else {   // 有缓冲队列,异步队列// Queue is full. Take the item at the// head of the queue. Make the sender enqueue// its item at the tail of the queue. Since the// queue is full, those are both the same slot.// 此时不能直接拷贝等待发送协程的数据,所以先读信道,然后将等待发送协程的数据拷贝到信道qp := chanbuf(c, c.recvx)if raceenabled {racenotify(c, c.recvx, nil)racenotify(c, c.recvx, sg)}// copy data from queue to receiverif ep != nil {typedmemmove(c.elemtype, ep, qp)}// copy data from sender to queuetypedmemmove(c.elemtype, qp, sg.elem)c.recvx++if c.recvx == c.dataqsiz {c.recvx = 0}c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz}sg.elem = nilgp := sg.gunlockf()gp.param = unsafe.Pointer(sg)sg.success = trueif sg.releasetime != 0 {sg.releasetime = cputicks()}goready(gp, skip+1)   // 缓存等待发送协程
}

从信道中接收数据的过程和发送数据的过程是相似的,总结一下:

  1. 首先是检查信道是否为nil(没有初始化),如果为nil,如果是非阻塞模式,则直接返回false,否则panic(用户使用的信道接收都是阻塞的)
  2. 如果处于非阻塞模式下,此时信道为空(无缓冲信道的为空指得是没有等待发送协程队列),则直接返回false表示发送失败,这样做的好处是避免加锁的开销,尽可能减少加锁范围内的代码量
  3. 加锁,再次检查信道是否已经被关闭,如果被关闭且信道中没有数据,则返回零值数据(如果信道被关闭但是缓冲里还有数据会被后面从循环队列中正常拷贝数据)
  4. 如果等待发送协程队列非空,说明此时信道的状态是满的,此时会直接从循环队列中拷贝出一个数据,然后再将一个等待发送协程的数据拷贝到循环队列中,并且将该协程唤醒,释放锁返回
  5. 如果判断信道状态非空(有缓冲),则从循环队列中拷贝一份数据,释放锁返回
  6. 如果为空,如果是非阻塞模式,则直接返回false,否则将自身状态更新为阻塞,加入该信道的等待接收队列中
  7. 接下来的代码执行的话,也就是说被唤醒了,此时正常返回

关闭操作

func closechan(c *hchan) {if c == nil {   // 关闭一个nil信道会panicpanic(plainError("close of nil channel"))}lock(&c.lock)   // 加锁if c.closed != 0 {   // 关闭一个已经被关闭的信道会panicunlock(&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// 唤醒所有等待发送或者等待接收的队列// release all readersfor {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.ggp.param = unsafe.Pointer(sg)sg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}glist.push(gp)}// release all writers (they will panic)// 唤醒等待发送队列,等待这些协程的是直接panicfor {sg := c.sendq.dequeue()if sg == nil {break}sg.elem = nilif sg.releasetime != 0 {sg.releasetime = cputicks()}gp := sg.ggp.param = unsafe.Pointer(sg)sg.success = falseif raceenabled {raceacquireg(gp, c.raceaddr())}glist.push(gp)}unlock(&c.lock)// Ready all Gs now that we've dropped the channel lock.for !glist.empty() {gp := glist.pop()gp.schedlink = 0goready(gp, 3)}
}

关闭信道的操作相对简单,总结如下:

  1. 检查信道是否为nil,,为nil则直接panic
  2. 加锁,检查信道是否已经被关闭,如果是,则panic
  3. 唤醒所有等待接收协程,唤醒所有等待发送协程(被唤醒后会panic)

select 操作

如果把信道作为一种io的话,那么select相当于是一种io多路复用机制,同时监听多个阻塞的信道。其实现原理正是通过非阻塞的对于信道的发送接收来实现的,对于每一个case,会被编译器编译为一个if-else结构,然后循环地遍历每一个信道,用非阻塞的模式尝试去读还是写,其实现代码如下:

// compiler implements
//
//	select {
//	case c <- v:
//		... foo
//	default:
//		... bar
//	}
//
// as
//
//	if selectnbsend(c, v) {
//		... foo
//	} else {
//		... bar
//	}
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {return chansend(c, elem, false, getcallerpc())
}// compiler implements
//
//	select {
//	case v, ok = <-c:
//		... foo
//	default:
//		... bar
//	}
//
// as
//
//	if selected, ok = selectnbrecv(&v, c); selected {
//		... foo
//	} else {
//		... bar
//	}
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {return chanrecv(c, elem, false)
}

同时可以看到,如果监听的某一个信道已经被关闭了,假设是读取操作,这个case也会命中,因为chanrecv函数返回的第一个参数会是true。

实验

字节面试常考的一道题目就是三个协程,分别打印A, B, C, 然后现在需要打印出ABCABCABC。实现代码如下,尽可能用有缓冲的队列,无缓冲队列很容易死锁,而golang语言自带死锁检测机制,检测到死锁会直接panic。

import ("sync""fmt"
)func main() {var wg sync.WaitGroupwg.Add(3)AChannel := make(chan struct{}, 1)BChannel := make(chan struct{}, 1)CChannel := make(chan struct{}, 1)go func() {defer wg.Done()for i := 0; i < 3; i++ {<- AChannelfmt.Print("A")BChannel <- struct{}{}}}()go func() {defer wg.Done()for i := 0; i < 3; i++ {<- BChannelfmt.Print("B")CChannel <- struct{}{}}}()go func() {defer wg.Done()for i := 0; i < 3; i++ {<- CChannelfmt.Print("C")AChannel <- struct{}{} }}()AChannel <- struct{}{}wg.Wait()
}

参考

  • Go 程序员面试笔试宝典

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/60452.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GPT中转站技术架构

本文介绍阿波罗AI中转站&#xff08;https://api.ablai.top/&#xff09;的技术架构&#xff0c;该中转API的技术架构采用了分布式架构、智能调度和API中转等技术&#xff0c;确保了全球范围内的高效访问和稳定运行。以下是对该技术架构的详细分析&#xff1a; 分布式架构 分…

【强化学习的数学原理】第02课-贝尔曼公式-笔记

学习资料&#xff1a;bilibili 西湖大学赵世钰老师的【强化学习的数学原理】课程。链接&#xff1a;强化学习的数学原理 西湖大学 赵世钰 文章目录 一、为什么return重要&#xff1f;如何计算return&#xff1f;二、state value的定义三、Bellman公式的详细推导四、公式向量形式…

[less] Operation on an invalid type

我这个是升级项目的时候遇到的&#xff0c;要从 scss 升级到 less&#xff0c;然后代码中就报了这个错误 我说一下代码的错误过程&#xff0c;但是这里没有复现&#xff0c;因为我原本报错的代码要复杂很多&#xff0c;而且是公司代码&#xff0c;不方便透露&#xff0c;这是我…

ssm面向品牌会员的在线商城小程序

摘要 随着Internet的发展&#xff0c;人们的日常生活已经离不开网络。未来人们的生活与工作将变得越来越数字化&#xff0c;网络化和电子化。它将是直接管理面向品牌会员的在线商城小程序的最新形式。本小程序是以面向品牌会员的在线商城管理为目标&#xff0c;使用 java技术制…

国土变更调查拓扑错误自动化修复工具的研究

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 一、拓扑错误的形成原因 1.边界不一致 2.不规则图形 3.尖锐角 4.局部狭长 5.细小碎面 6.更新层相互重叠 二、修复成果展示 1.边界不一致 2.不规则图形 3.尖锐角 4.局部狭…

WPS 加载项开发说明wpsjs

wpsjs几个常用的CMD命令&#xff1a; 1.打开cmd输入命令测试版本号 npm -v 2.首次安装nodejs&#xff0c;npm默认国外镜像&#xff0c;包下载较慢时&#xff0c;可切换到国内镜像 //下载速度较慢时可切换国内镜像 npm config set registry https://registry.npmmirror.com …

Javaweb梳理18——JavaScript

今日目标 掌握 JavaScript 的基础语法掌握 JavaScript 的常用对象&#xff08;Array、String&#xff09;能根据需求灵活运用定时器及通过 js 代码进行页面跳转能通过DOM 对象对标签进行常规操作掌握常用的事件能独立完成表单校验案例 18.1 JavaScript简介 JavaScript 是一门跨…

android 使用MediaPlayer实现音乐播放--权限请求

在Android应用中&#xff0c;获取本地音乐文件的权限是实现音乐扫描功能的关键步骤之一。随着Android版本的不断更新&#xff0c;从Android 6.0&#xff08;API级别23&#xff09;开始&#xff0c;应用需要动态请求权限&#xff0c;而到了android 13以上需要的权限又做了进一步…

GPT系列文章

GPT系列文章 GPT1 GPT1是由OpenAI公司发表在2018年要早于我们之前介绍的所熟知的BERT系列文章。总结&#xff1a;GPT 是一种半监督学习&#xff0c;采用两阶段任务模型&#xff0c;通过使用无监督的 Pre-training 和有监督的 Fine-tuning 来实现强大的自然语言理解。在 Pre-t…

NUXT3学习日记四(路由中间件、导航守卫)

前言 在 Nuxt 3 中&#xff0c;中间件&#xff08;Middleware&#xff09;是用于在页面渲染之前或导航发生之前执行的函数。它们允许你在路由切换时执行逻辑&#xff0c;像是身份验证、重定向、权限控制、数据预加载等任务。中间件可以被全局使用&#xff0c;也可以只在特定页…

汽车免拆诊断案例 | 2012款路虎揽胜运动版柴油车加速无力

故障现象  一辆2012款路虎揽胜运动版车&#xff0c;搭载3.0T柴油发动机&#xff08;型号为306DT&#xff09;&#xff0c;累计行驶里程约为10.2万km。车主进厂反映&#xff0c;车辆行驶中加速无力&#xff0c;且发动机故障灯异常点亮。 故障诊断 接车后试车&#xff0c;发动…

网络安全与加密

1.Base64简单说明描述&#xff1a;Base64可以成为密码学的基石&#xff0c;非常重要。特点&#xff1a;可以将任意的二进制数据进行Base64编码结果&#xff1a;所有的数据都能被编码为并只用65个字符就能表示的文本文件。65字符&#xff1a;A~Z a~z 0~9 / 对文件进行base64编码…

C语言:数组转换指针的时机

1、指针数组 如果一个数组中的所有元素保存的都是指针&#xff0c;那么我们就称它为指针数组&#xff0c;指针数组的定义形式一般为&#xff1a; dataType *arrayName[length];[ ]的优先级高于*&#xff0c;该定义形式应该理解为&#xff1a; dataType *(arrayName[length])…

UE5 DownloadImage加载jpg失败的解决方法

DownloadImage加载jpg失败的解决方法 现象解决方案具体方法 现象 用UE自带的 DownloadImage 无法下载成功&#xff0c;从 failure 引脚出来。 接入一个由监控器自动保存起的图像&#xff0c;有些可以正常加载成功&#xff0c;有些无法加载成功。 经调查问题出现在&#xff0c;…

使用 helm 部署 gitlab

一、下载 Gitlab chart 进入 artifacthub 官网 选择你想要的版本&#xff08;我选择的chart版本是 8.4.0 , gitlab 版本是17.4.0 &#xff09; 进入到控制台&#xff0c;添加helm仓库 如果你想不改任何配置&#xff0c;你可以执行安装命令&#xff0c;等待安装即可helm instal…

FreeRTOS信号量(一)

目录 什么是信号量&#xff1f; 1.信号量简介 2.二值信号量 2.1二值信号量简介 1. 首先&#xff0c;创建时&#xff0c;二值信号量默认无效 2. 之后中断释放信号量 3.信号量获取成功 4、任务再次进入阻塞态 2.2 创建二值信号量 1、函数vSemaphoreCreateBinary () 2、…

51单片机-独立按键与数码管联动

独立键盘和矩阵键盘检测原理及实现 键盘的分类&#xff1a;编码键盘和非编码键盘 键盘上闭合键的识别由专用的硬件编码器实现&#xff0c;并产生键编码号或键值的称为编码键盘&#xff0c;如&#xff1a;计算机键盘。靠软件编程识别的称为非编码键盘&#xff1b;在单片机组成…

springboot课程答疑系统(代码+数据库+LW)

摘要 随着信息互联网信息的飞速发展&#xff0c;无纸化作业变成了一种趋势&#xff0c;针对这个问题开发一个专门适应师生交流形式的网站。本文介绍了课程答疑系统的开发全过程。通过分析企业对于课程答疑系统的需求&#xff0c;创建了一个计算机管理课程答疑系统的方案。文章…

解锁业务成功:大数据和 AI 如何协作以释放战略洞察

在当今这个数据主导的时代&#xff0c;大数据与AI的协同作用对于寻求竞争优势的组织而言愈发关键。大数据以其庞大的数据量、多样化的数据类型以及高速的数据生成能力&#xff0c;为AI算法提供了丰富的原材料&#xff0c;助力其挖掘出有价值的洞见&#xff0c;推动明智决策的制…

24.UE5枚举,怪物分类,龙卷风技能

2-26 枚举、怪物分类、龙旋风技能、掉落概率_哔哩哔哩_bilibili 目录 1.枚举 1.1枚举类型的创建 1.2 将枚举类型绑定到怪物蓝图上 1.3枚举类型的使用 1.3.1创建新的掉落物 1.3.2更改怪物掉落逻辑 2.龙卷风技能 2.1输入映射 2.2龙卷风发射物的创建 2.3龙卷风伤害逻辑…