Golang的channel底层实现个人走读

1.channel简介

channel用于Goroutine之间的通信,它提供了一种在不同协程之间传递数据的机制。channel是一种类型安全、阻塞的、先进先出(FIFO)的数据结构,确保发送的数据按照发送的顺序接收。

Go语言提供通过通信来共享内存,而不是通过共享内存来通信.(个人理解就是我跟你用的是同样的东西,但是这个东西你有一份,我也有一份,也可以理解是我把要通信的东西拷贝了一份)

2.核心数据结构 

go version go1.19.3 linux/amd64

2.1 hchan

channel分为无缓冲和有缓冲两种,有缓冲的channel使用ring buffer(环形缓冲区)来缓存写入的数据,本质是循环数组(为啥用循环数组?普通数组容量固定、更适合指定的空间,且弹出元素时,元素需要全部前移)

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 mutex
}
  • qcount:循环数组中元素的数量
  • dataqsiz:循环数组中的大小,容量
  • buf:指向底层环形数组的指针(环形缓冲区)
  • elemsize:元素的大小
  • closed:是否关闭的标志:0:未关闭,1:已关闭
  • elemtype:channel中的元素类型
  • sendx:下一次写的位置(当下标超过数组容量最后会回到第一个位置,所以需要有两个字段记录当前写的下标位置)
  • recvx:写一次读的位置(当下标超过数组容量最后会回到第一个位置,所以需要有两个字段记录当前写的下标位置)
  • recvq:读等待协程队列
  • sendq:写等待协程队列
  • lock:互斥锁,保证读写channel时并发安全问题

2.2 sendq和recvq

sendq和recvq分别表示阻塞在当前channel上的发送者goroutine和接收者goroutine。其实现为双链表,双链表对应的结构时waitq,节点是sudog。结构如下:

type waitq struct {
	first *sudog
	last  *sudog
}

双链表的节点是sudog,sudog是Goroutine抽象出来的结构体,一个sudog代表一个在等待队列中的g。sudog主要记录了哪个协程正在等待;等待发送/接收的数据在哪里;等待哪个channel;因为Goroutine与同步对象关系是多对多的。

// src/runtime/chan.go
type sudog struct {
    // 记录哪个协程在等待
	g *g

	next *sudog
	prev *sudog
    // 等待发送/接受的数据在哪里
	elem unsafe.Pointer // data element (may point to stack)



	acquiretime int64
	releasetime int64
	ticket      uint32


	isSelect bool

    // 接收/发送数据是否成功
	success bool

	parent   *sudog // semaRoot binary tree
	waitlink *sudog // g.waiting list or semaRoot
	waittail *sudog // semaRoot
    // 等待的是哪个channel
	c        *hchan // channel
}

3. channel的创建

创建channel实际上就是调用runtime.makechan()函数在内存中实例化出一个hchan的结构体,并返回一个指向该结构体的指针,所以channel是引用类型。

3.1 makechan创建channel整体流程 

创建channel时主要分为两大块:边界检查和分配内存,分配内存流程如下:

  • 如果是无缓冲channel,直接给hchan结构体分配内存并返回指针
  • 如果是有缓冲的channel,但是元素不包含指针类型,则一次性为hchan结构体和底层循环数组分配连续内存并返回指针(需要连续内存)
  • 如果是有缓冲的channel,并且元素包含指针类型,则分别分配hchan结构体内存和底层循环数组的内存并返回指针(可以利用内存碎片)

3.2 makechan源码 

func makechan(t *chantype, size int) *hchan {
    // 获取元素
	elem := t.elem

    // 元素的大小必须小于64k
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
    // 检查是否对齐,设计操作系统的知识
	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
		throw("makechan: bad alignment")
	}
    // 计算buf所需要的内存大小
	mem, overflow := math.MulUintptr(elem.size, uintptr(size))
	if overflow || mem > maxAlloc-hchanSize || size < 0 {
		panic(plainError("makechan: size out of range"))
	}
    // 创建hchan指针
	var c *hchan
	switch {
	case mem == 0:
        // 队列buf或者元素大小为0(struct{}元素大小为0):只分配hchan的内存
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.ptrdata == 0:
		// buf里的元素不包含指针类型
        // 一次性为hchan和buf分配连续的内存
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	default:
		// 元素包含指针类型,分别分配hchan结构体和底层循环数组的内存
		c = new(hchan)
		c.buf = mallocgc(mem, elem, true)
	}

    // 设置hchan中的elemsuze、elemtype、dataqsiz、lock等属性
	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)

    // 如果启用了debugChan,进行调试
	if debugChan {
		print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
	}
    // 返回hchan指针
	return c
}

4.写流程

当我们向一个channel发送数据时,其实底层就是调用了runtime.chansend1()函数

 chansend1()函数如下:

func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

其实主要逻辑还是在chansend()这个函数,我们平常直接使用 ch <- 1,其实就是使用了chansend的阻塞模式。还有非阻塞模式也就是搭配上select case使用channel。下面先介绍阻塞模式。

4.1chansend() -- block

4.1.1 总流程

4.1.2 chansend()源码 

chansend接收到的4个参数:

  • c:是一个指向hchan类型的指针,表示要接受数据的通道
  • ep:是一个unsafe.Pointer类型的指针,接收要发送的数据的地址
  • block:表示接收操作的模式。true为阻塞模式,即发送操作将会阻塞,直到有接收者接受元素;false表示非阻塞模式,及发送操作不会阻塞,如果通道已满,发送操作会立即返回(非阻塞模式后面介绍)
  • callerpc:发送操作的调用者的程序计数器值
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 如果channel为nil,也就是未初始化,此时写数据直接死锁并且throw
	if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
	...
	lock(&c.lock)

    // 如果channel已经关闭,此时写数据会panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

    // 如果阻塞读队列不为空,也就是有等待的接收者
	if sg := c.recvq.dequeue(); sg != nil {
		// 直接copy一份ep发送给接收者,不用放入buf
		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指向循环数组中未使用的位置
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			racenotify(c, c.sendx, nil)
		}
        // 将发送的数据copy一份写入到qp指向的环形数组中的位置
		typedmemmove(c.elemtype, qp, ep)
        // 更新下一次写入环形数组的元素index
		c.sendx++
        // 当最后一个元素已经使用,此时环形队列将再次从0开始循环
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
        // 队列中的元素数量+1
		c.qcount++
		unlock(&c.lock)
		return true
	}

	...

	// Block on the channel. Some receiver will complete our operation for us.
    // 获取当前的Goroutine,用于绑定sudog
	gp := getg()
    // 获取一个sudog
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// 保存要发送的数据的地址
	mysg.elem = ep
	mysg.waitlink = nil
    // 保存当前的Goroutine
	mysg.g = gp
    // 没有使用select case
	mysg.isSelect = false
    // 保存当前的channel
	mysg.c = c
	gp.waiting = mysg
	gp.param = nil
    // 把sudog放进写阻塞队列
	c.sendq.enqueue(mysg)

	atomic.Store8(&gp.parkingOnChan, 1)
    // 调用gopark函数将当前Goroutine挂起,状态为_Gwaiting,等待读协程唤醒或者解锁时唤醒
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 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)

	// someone woke us up.
	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
    // 清除sudog
	releaseSudog(mysg)
    // 检查是否channel已经关闭,关闭就panic
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}

send()函数如下:

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	if raceenabled {
		if c.dataqsiz == 0 {
			racesync(c, sg)
		} else {
			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 {
        // 有阻塞的读协程,直接copy一份要发送的数据ep到阻塞的读协程
		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)
}

// 把sg的数据直接copy一份到src
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)
}

5.读流程

当我们用一个channel接收数据,其实底层就是调用chanrecv1()

chanrecv1()如下:

func chanrecv1(c *hchan, elem unsafe.Pointer) {
	chanrecv(c, elem, true)
}

核心逻辑在chanrecv()这个函数,下面还是以阻塞模式下介绍。

5.1 chanrecv() - block

5.1.1 总流程

5.1.2 chanrecv()源码 
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	if debugChan {
		print("chanrecv: chan=", c, "\n")
	}
    // 如果channel未初始化,直接死锁并且throw
	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	...

	lock(&c.lock)

    // 如果channel已经关闭,直接返回
	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 {
            // 接着判断buf有无元素,
            // 有就从buf取出元素copy一份放到ep,然后把阻塞写协程的数据写到buf
            // 没有就直接从阻塞写协程copy一份数据到ep
			recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
			return true, true
		}
	}

    // 如果buf不为空
	if c.qcount > 0 {
		// 获取要读取的数据在buf中的位置
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
		}
        // 把读取出来的数据copy一份到ep
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
        // 清除刚才从buf读取的数据
		typedmemclr(c.elemtype, qp)
        // 更新buf下一次要读取数据的index
		c.recvx++
        // 如果到了buf的最后一个,重置为0
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
        // buf元素-1
		c.qcount--
		unlock(&c.lock)
		return true, true
	}

	...
    // 获取当前的Goroutine,用于绑定sudog
	gp := getg()
    // 获取一个sudog
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
    // 保存读取数据的地址
	mysg.elem = ep
	mysg.waitlink = nil
	gp.waiting = mysg
    // 保存当前的Goroutine
	mysg.g = gp
	mysg.isSelect = false
    // 保存当前的channel
	mysg.c = c
	gp.param = nil
    // 把sudog放入阻塞读队列
	c.recvq.enqueue(mysg)
	atomic.Store8(&gp.parkingOnChan, 1)
    // 调用gopark阻塞Goroutine,把状态切换成_Gwaiting,等待被唤醒
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)

	// someone woke us up
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	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
}

recv()如下:

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    // 如果buf没有元素
	if c.dataqsiz == 0 {
		if raceenabled {
			racesync(c, sg)
		}
        // copy一份阻塞写协程的数据到ep
		if ep != nil {
			// copy data from sender
			recvDirect(c.elemtype, sg, ep)
		}
	} else {
        // 如果buf有元素
        // 先获取要读buf的哪一条数据
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			racenotify(c, c.recvx, nil)
			racenotify(c, c.recvx, sg)
		}
		// copy data from queue to receiver
        // 然后把从buf中获取到的数据copy一份到ep
		if ep != nil {
			typedmemmove(c.elemtype, ep, qp)
		}
		// copy data from sender to queue
        // 然后把阻塞写协程的数据写的buf
		typedmemmove(c.elemtype, qp, sg.elem)
        // 更新buf下一次读取数据的index
		c.recvx++
        // 超过buf的最后一条数据index,重置c.recvx为0
		if c.recvx == c.dataqsiz {
			c.recvx = 0
		}
		c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
	}
	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)
}

// 把sg的数据copy一份到dst
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	src := sg.elem
	typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
	memmove(dst, src, t.size) // 汇编实现,有兴趣自己看
}

// 把src的数据copy一份到dst
func typedmemmove(typ *_type, dst, src unsafe.Pointer) {
	if dst == src {
		return
	}
	if writeBarrier.needed && typ.ptrdata != 0 {
		bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.ptrdata)
	}
	memmove(dst, src, typ.size)
	if writeBarrier.cgo {
		cgoCheckMemmove(typ, dst, src, 0, typ.size)
	}
}

6.阻塞和非阻塞模式

上面的源码均是以阻塞模式为主线进行讲述,忽略了非阻塞模式的有关处理逻辑

6.1 非阻塞模式逻辑上的区别

非阻塞模式下,读写channel方法通过一个bool型的响应参数,用以标识是否读取、写入成功。

  • 所有需要使得当前Goroutine被挂起的操作,在非阻塞模式都会直接返回false
  • 所有使得当前Goroutine会进入死锁的操作,在非阻塞模式下都会直接返回false
  • 所有能立即完成读取、写入操作的条件下,非阻塞模式下会返回true

6.2 何时进入阻塞模式

默认情况下,读写channel都是阻塞模式,只有在select语句组成的多路复用分支中,channel才会变成非阻塞模式:

ch := make(chan int)
select {
    case <- ch:
    default:
}

6.3 底层实现

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)
}

 同样是调用chanrecv和chansend函数,但是,此时第三个参数传入的block是false,这就导致后续流程会进入非阻塞模式

7. 两种读channel的协议

读取channel时,可以根据第二个bool型的返回值用以判断当前channel是否已经处于关闭状态:

ch := make(chan int,2)
get1 := <- ch
get2, ok := <- ch

底层调用如下:

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
}

8. close

close一个channel,底层实际上调用了runtime.closechan()函数

8.1 总流程 

8.2 closechan源码 

这里之所以用了两个for循环,是因为不可能阻塞的读协程队列和写协程队列都同时存在数据。

可以这样假设,

1.假设阻塞的写协程队列有数据,然后有读协程向channel读取数据,此时会进入recv()函数,buf没有数据就直接copy阻塞的写协程的数据到ep,有数据就直接把buf的数据copy一份到ep,然后再把阻塞写协程的数据写入buf,最后唤醒阻塞的写协程,读协程压根就没机会阻塞。

2.假设阻塞的读协程队列有数据,然后有写协程给channel发送数据,此时会进入send()函数,同理,一样没机会

还需要注意的是,close channel之前,如果有阻塞的写协程会panic,看4.1.2源码最后

func closechan(c *hchan) {
    // 关闭一个未初始化的channel,直接panic
	if c == nil {
		panic(plainError("close of nil channel"))
	}

	lock(&c.lock)
    // 关闭一个已经关闭的channel,直接panic
	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())
	}
    
    // 设置channel为1,代表关闭,0表示未关闭
	c.closed = 1

	var glist gList

	// release all readers
    // 获取所有阻塞的读协程
	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)
	}

	// release all writers (they will panic)
    // 获取所有阻塞的写协程
	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)

	// Ready all Gs now that we've dropped the channel lock.
    // 唤醒阻塞的协程
	for !glist.empty() {
		gp := glist.pop()
		gp.schedlink = 0
		goready(gp, 3)
	}
}

9.gopark&goready

gopark就是阻塞当前的Goroutine,源码如下:

func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
	if reason != waitReasonSleep {
		checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
	}
    // 给m加锁
	mp := acquirem()
	gp := mp.curg
	status := readgstatus(gp)
	if status != _Grunning && status != _Gscanrunning {
		throw("gopark: bad g status")
	}
    // 保存当前Goroutine的运行信息已经waiting reason
	mp.waitlock = lock
	mp.waitunlockf = unlockf
	gp.waitreason = reason
	mp.waittraceev = traceEv
	mp.waittraceskip = traceskip
    // 给m解锁
	releasem(mp)
	// can't do anything that might move the G between Ms here.
	mcall(park_m)
}

func acquirem() *m {
	_g_ := getg()
	_g_.m.locks++
	return _g_.m
}

接着看park_m():

func park_m(gp *g) {
	_g_ := getg()

	if trace.enabled {
		traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
	}

    // 变更Goroutine状态为_Gwaiting
	casgstatus(gp, _Grunning, _Gwaiting)
    // 把当前Goroutine和M解绑
	dropg()

	if fn := _g_.m.waitunlockf; fn != nil {
		ok := fn(gp, _g_.m.waitlock)
		_g_.m.waitunlockf = nil
		_g_.m.waitlock = nil
		if !ok {
			if trace.enabled {
				traceGoUnpark(gp, 2)
			}
			casgstatus(gp, _Gwaiting, _Grunnable)
			execute(gp, true) // Schedule it back, never returns.
		}
	}
    // 开启新一轮调度
	schedule()
}

goready源码如下:

func goready(gp *g, traceskip int) {
	systemstack(func() {
		ready(gp, traceskip, true)
	})
}

接着看ready如下:

func ready(gp *g, traceskip int, next bool) {
	if trace.enabled {
		traceGoUnpark(gp, traceskip)
	}

	status := readgstatus(gp)

	// Mark runnable.
	_g_ := getg()
    // 给m加锁
	mp := acquirem() // disable preemption because it can be holding p in a local var
	if status&^_Gscan != _Gwaiting {
		dumpgstatus(gp)
		throw("bad g->status in ready")
	}

	// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
    // 修改Goroutine的状态为_Grunnable
	casgstatus(gp, _Gwaiting, _Grunnable)
    // 把Goroutine放入本地队列(有概率被放入runnext)
	runqput(_g_.m.p.ptr(), gp, next)
    // 尝试通过cas唤醒p,让它去找空闲的M组合,然后寻找G调度
	wakep()
    // 给m解锁
	releasem(mp)
}

在channel实现中,协程阻塞和被唤醒,其实就是在阻塞Goroutine之前,把Goroutine的信息保存好,封装到sudog,然后把sudog放入阻塞队列,然后就更改Goroutine状态为_Gwaiting,然后解除Goroutine和M的关系,直接开启下一轮调度,不需要管这个被抛弃的Goroutine。等这个Goroutine满足某些条件自然就会被唤醒,简单举个例子,此时有一个无缓冲的channel,然后也没有阻塞的读协程,然后有个协程向此channel发送数据,然后就会被封装成sudog放到阻塞的写协程队列,然后有协程来此channel读取数据,会直接把刚才被阻塞都的那个协程的sudog拿出来,copy一份sudog里面的要写的数据到读数据存放地址ep,然后会调用goready()把这个阻塞的协程状态切换成_Grunnable,然后放入本地队列(有概率先放入runnext),然后等待执行。

10. 引用

Golang分享(一):channel底层原理 - 知乎 (zhihu.com)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值