Golang源码阅读笔记 - Channel

底层数据结构

type hchan struct {
	qcount   uint  // channel以有数据大小
	dataqsiz uint  // channel大小
	buf      unsafe.Pointer // 有缓存的channel,其buf地址
	elemsize uint16
	closed   uint32
	elemtype *_type // channel中元素类型
	sendx    uint   // 发送队列索引
	recvx    uint   // 接收队列索引
	recvq    waitq  // 接收队列;读协程等待列表
	sendq    waitq  // 发送队列;写协程等待列表
	lock mutex // channel全局操作时需要有锁住
}

type waitq struct {
	firsxt *sudog
	last  *sudog
}
  1. channel底层
    • buf 缓存通道数据
    • recvq/sendq 存放阻塞的接收协程/发送协程
  2. sudog表示一个在等待队列中的g对象,是一个双向链表,主要包含协程本身,和前后指针
    • g *g
    • next *sudog
    • prev *sudog
  3. waitq是一个包含sudog的双向链表,在sudog的基础上,增加首位指针

channel等待队列读写

func (q *waitq) enqueue(sgp *sudog) {
	sgp.next = nil
	x := q.last
	if x == nil {
		sgp.prev = nil
		q.first = sgp
		q.last = sgp
		return
	}
	sgp.prev = x
	x.next = sgp
	q.last = sgp
}

func (q *waitq) dequeue() *sudog {
	for {
		sgp := q.first
		if sgp == nil {
			return nil
		}
		y := sgp.next
		if y == nil {
			q.first = nil
			q.last = nil
		} else {
			y.prev = nil
			q.first = y
			sgp.next = nil // mark as removed (see dequeueSudog)
		}
		...
		return sgp
	}
}
  1. channel等待队列sendq, recvq均为双向链表
  2. 队列插入enqueue: 双向链表插入操作,看起来没有上限
  3. 队列取出dequeue: 双向链表取出操作, 队列为空是返回nil
    • for循环是处理在select语句中的阻塞操作

channel新建

func makechan(t *chantype, size int) *hchan {
	// 计算和校验需要申请的buf内存大小
	elem := t.elem
	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 {
	// 零缓存通道,无需buffer,只用申请hchan本身大小即可
	case mem == 0: 
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		c.buf = c.raceaddr()
	// elem不包含指针???不懂
	case elem.ptrdata == 0: 
		c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
		c.buf = add(unsafe.Pointer(c), hchanSize)
	// 默认情况下,创建一个hchan结构体,然后申请buf内存
	default:
		c = new(hchan)  
		c.buf = mallocgc(mem, elem, true)
	}

	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	return c
}
  1. channel新建,整体上即初始化hchan结构体和buff需要的内存
    • buff区没有结构,只是一段通用的内存,然后通过指针进行寻址

channel发送消息

// channel单词发送逻辑(这里只是尝试一次消息发送)
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
	// 如果不阻塞、chan未关闭、buff已满,直接返回false(发送失败)
	if !block && c.closed == 0 && full(c) {
		return false
	}
	
	// 全局加锁
	lock(&c.lock)
	// 如果此时chan是关闭状态(其他协程关闭),panic
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}
	
	// 从chan接收队列中取出一个协程(该协程在接收chan消息时被放进recvq)
	// 如果接收队列中存在有效协程,则直接将消息内容复制到取出协程的栈区(sg.elem),并返回发送成功
	if sg := c.recvq.dequeue(); sg != nil {
		send(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true
	}
	
	// 走到这里,说明接收队列为空了
	// 如果当前消息数小于chan缓存大小,将数据复制到chan缓冲区,同时注意增加sendx,返回发送成功
	if c.qcount < c.dataqsiz {
		qp := chanbuf(c, c.sendx)
		typedmemmove(c.elemtype, qp, ep)
		c.sendx++
		if c.sendx == c.dataqsiz {
			c.sendx = 0
		}
		c.qcount++
		unlock(&c.lock)
		return true
	}
	
	// 这里,说明不仅接收队列空了,chan缓存也满了
	// 如果不阻塞,直接返回发送失败
	if !block {
		unlock(&c.lock)
		return false
	}
	
	// 如果需要阻塞,则新组装sudog对象,并将sudog写入chan.sendq (此时发送协程处于阻塞态)
	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)
	...
	return true
}

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

// 从src地址中,赋值t大小数据到sg.elem - 协程栈数据区
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
	dst := sg.elem
	...
	memmove(dst, src, t.size)
}

// memmove copies n bytes from "from" to "to".
func memmove(to, from unsafe.Pointer, n uintptr)

总结:

  1. channel发送消息优先级:
    • 接收队列中取waiting goroutine,存在则直接将数据复制到接收协程的数据区(elem)
    • waiting gorouting,则尝试将数据写入channel缓冲区暂存
    • 如果channel 缓冲区也满了,则看调用方是否需要阻塞(调用方是谁???)
      • 不阻塞,直接返回数据写入失败
      • 阻塞,则新建sudog,并同步数据指针,然后存入channel sendq(从enqueue的代码看,sendq/waitq大小无上限限制)
  2. 消息发送成功的底层逻辑:
    • 不管是直接发送给接收协程,还是写入channel缓冲,消息传递的方式都是数据的复制

channel接收消息

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
	// 如果sendq中有等待发送数据的协程,则执行recv,会根据channel是否有buffer做不同处理
	// 无buffer,则直接将等待队列中goroutine数据复制给接收者
	// 有buffer,则从buffer中获取数据给接受者,再把待发送goroutine的数据写到buffer里
	if sg := c.sendq.dequeue(); sg != nil {
		recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
		return true, true
	}
	
	// 没有等待的发送协程
	// 如果此时channel buffer中有数据,从buffer中取出数据并赋值给ep
	if c.qcount > 0 {
		qp := chanbuf(c, c.recvx)
		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
	}
	
	// 否则新组装sudog,并将其enqueue到recvq中
	// 接收协程也出于阻塞状态
	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)
	...
	return true, !closed
}

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
	// 如果是无缓存channel,直接把sudog的数据拷贝给ep地址
	if c.dataqsiz == 0 {
		if raceenabled {
			racesync(c, sg)
		}
		if ep != nil {
			recvDirect(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.
		
		// 从channel buffer中取出一条数据,并拷贝给目的地址ep
		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 // c.sendx = (c.sendx+1) % c.dataqsiz
	}
	...
}

// 从sudog的数据区中,拷贝一个对象到dst。这一个对象其实就是该类型的字节数大小
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
	src := sg.elem
	...
	memmove(dst, src, t.size)
}

总结:

  1. channel接收消息优先级:
    • 发送队列中有等待的发送协程,则直接将该发送协程携带的数据复制给接收指针
    • 如果无等待中的发送队列,则从channel buffer中取出数据复制给接收指针
    • 如果channel buffer为空,根据调用方是否需要阻塞:
      • 不阻塞,直接返回接收失败
      • 阻塞,新组装sudog,并将其加入recvq,此时接收数据的协程也出于阻塞状态

其他函数

// 根据索引值,寻找channel内数据的指针位置
func chanbuf(c *hchan, i uint) unsafe.Pointer {
	return add(c.buf, uintptr(i)*uintptr(c.elemsize))
}

// 判断缓存channel是否已满
func full(c *hchan) bool {
	if c.dataqsiz == 0 {
		return c.recvq.first == nil
	}
	return c.qcount == c.dataqsiz // 已有数据量是否等于channel容量
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值