Channel的实现原理

本文详细剖析了Go语言中的channel数据结构,包括hchan、waitq和sudog等组成部分,以及makechan函数的实现。重点讲解了chansend和chanrecv函数的读写流程,展示了goroutine如何在channel上高效地进行并发通信。
摘要由CSDN通过智能技术生成

1. 数据结构

hchan
type hchan struct {
	qcount   uint           // channel内元素的数量
	dataqsiz uint           // channel的容量
	buf      unsafe.Pointer // channel中元素队列(环形数组)
	elemsize uint16			// channel中元素类型大小
	closed   uint32			// channel关闭标识
	elemtype *_type 		// channel中元素类型
	sendx    uint   		// channel写入元素的index
	recvx    uint   		// channel读取元素的index
	recvq    waitq  		// channel阻塞读队列
	sendq    waitq  		// channel阻塞写队列
	lock mutex				// channel锁
}
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

	parent   *sudog // semaRoot binary tree
	waitlink *sudog // g.waiting list or semaRoot
	waittail *sudog // semaRoot
	c        *hchan // channel
}
  • g *g:指向相关的 Goroutine。

  • next *sudogprev *sudog:用于构成一个链表,用于管理等待在同一个 channel 上的多个 Goroutine。

  • elem unsafe.Pointer:与等待的事件相关的数据元素。对于 channel,它可能指向 channel 上的数据;对于 select 语句,它可能指向 select case。

  • acquiretime int64releasetime int64:用于记录 Goroutine 获取锁的时间和释放锁的时间。

  • ticket uint32:用于竞争获取锁时的票据,类似于一个简单的等待队列。

  • isSelect bool:指示 Goroutine 是否参与了 select 操作。

  • success bool:用于标记通信是否成功。如果为 true,表示 Goroutine 因为成功接收到值而被唤醒;如果为 false,表示 Goroutine 因为 channel 被关闭而被唤醒。

  • parent *sudogwaitlink *sudogwaittail *sudog:用于构建 Goroutine 等待队列的二叉树结构。在 channel 操作中,它们通常与 channel 的等待列表相关联。

  • c *hchan:指向相关的 channel。

它允许 Goroutine 在等待 channel 上的发送或接收操作时,以一种高效的方式挂起和恢复,同时支持与 select 语句的集成。通过这个结构体,Go 语言实现了高效的并发编程模型。

makechan
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:
		// 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
}

1. 检查元素类型的大小是否合法,

2. 检查 hchan 结构体的大小是否符合对齐要求,如果不符合,则抛出异常。

3. 判断申请内存空间大小是否越界,mem 大小为 element 类型大小与 element 个数相乘后得到,仅当无缓冲型 channel 时,因个数为 0 导致大小为 0;

4. 根据类型,初始 channel,分为 无缓冲型、有缓冲元素为 struct 型、有缓冲元素为 pointer 型 channel;

5. 倘若为无缓冲型,则仅申请一个大小为默认值 96 的空间;

6. 如若有缓冲的 struct 型,则一次性分配好 96 + mem 大小的空间,并且调整 chan 的 buf 指向 mem 的起始位置;

7. 倘若为有缓冲的 pointer 型,则分别申请 chan 和 buf 的空间,两者无需连续;

8. 对 channel 的其余字段进行初始化,包括元素类型大小、元素类型、容量以及锁的初始化.

2.读写流程

写流程:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	if c == nil {
		if !block {
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	if debugChan {
		print("chansend: chan=", c, "\n")
	}

	if raceenabled {
		racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
	}


	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 {
		// 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 {
		unlock(&c.lock)
		return false
	}

	// Block on the channel. Some receiver will complete our operation for us.
	gp := getg()
	mysg := acquireSudog()
	mysg.releasetime = 0
	if t0 != 0 {
		mysg.releasetime = -1
	}
	// No stack splits between assigning elem and enqueuing mysg
	// on gp.waiting where copystack can find it.
	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)
	
	atomic.Store8(&gp.parkingOnChan, 1)
	gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
	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
	releaseSudog(mysg)
	if closed {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	return true
}
  • 首先,函数检查是否有空闲的 channel 缓冲区,如果有,则直接将元素放入缓冲区,并返回 true。
  • 如果缓冲区已满,且不允许阻塞发送,则立即返回 false。
  • 如果允许阻塞发送,则函数会创建一个 sudog 结构体,表示等待发送的 goroutine,并将其加入到 channel 的发送队列中。
  • 然后,当前 goroutine 被挂起(通过 gopark 函数),等待其他 goroutine 接收数据或 channel 关闭。
  • 当有接收者接收到数据或者 channel 关闭时,阻塞的 goroutine 被唤醒,继续执行。
  • 如果 channel 已关闭但仍有发送者,则会抛出异常。

(用小徐老师的两张图)

读流程:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	if debugChan {
		print("chanrecv: chan=", c, "\n")
	}

	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}

	// Fast path: check for failed non-blocking operation without acquiring the lock.
	if !block && empty(c) {
		if atomic.Load(&c.closed) == 0 {
			return
		}
		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 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
		}
		// 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 {
			recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
			return true, true
		}
	}

	if c.qcount > 0 {
		// Receive directly from queue
		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
	}

	// no sender available: block on this channel.
	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)

	atomic.Store8(&gp.parkingOnChan, 1)
	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
}
  • 首先,函数检查是否有待接收的元素,如果有,则直接从 channel 中获取元素,并返回 true。
  • 如果没有待接收的元素,且不允许阻塞接收,则立即返回 false。
  • 如果允许阻塞接收,则函数会创建一个 sudog 结构体,表示等待接收的 goroutine,并将其加入到 channel 的接收队列中。
  • 然后,当前 goroutine 被挂起(通过 gopark 函数),等待其他 goroutine 发送数据或 channel 关闭。
  • 当有发送者发送数据或者 channel 关闭时,阻塞的 goroutine 被唤醒,继续执行。
  • 如果 channel 已关闭且为空,则函数返回 true 表示成功接收到数据,并返回 false 表示 channel 已关闭且为空。
  • 如果 channel 已关闭但仍有待接收的元素,则从队列中接收数据并返回 true。
  • 如果 channel 仍有缓冲区,则直接从缓冲区中接收数据并返回 true。
  • 如果 channel 为空且没有其他发送者,则阻塞的 goroutine 被挂起,等待其他 goroutine 发送数据或 channel 关闭。
  • 当有其他 goroutine 发送数据或 channel 关闭时,阻塞的 goroutine 被唤醒,继续执行并返回接收是否成功的状态。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值