go 通道 返回_源码角度解析Go语言并发[3]---Go如何实现CSP模型中"内存共享"...

写在前头:

  1. 代码解析写在代码中:用【】的形式,【xxxxxxxx】;
  2. 需要拓展的解析:用【1】序号的形式,【1】、【2】;然后在源码之后给出解释;
  3. 每个【】中解释的是:当前【】之下,直到下一个【】之间的代码段;

上图同样来自bing每日壁纸~

最近的文章有点偏题了。我们回到并发模型,上次发了两篇文章介绍并发模型——CSP

甘蔗:go语言并发原理和机制【一】​zhuanlan.zhihu.com
b3866aeefbd76a335b06f8b45313a1dd.png
甘蔗:go语言并发原理和机制【二】​zhuanlan.zhihu.com
0b031d7c1d5c775614cbde847b2cbeec.png

我们知道CSP模型中的精髓就是:

Don't communicate by sharing memory, share memory by communicating.

那么go语言是如何实现上述“利用通信进行内存共享”的呢?答案是利用通道。

通道(channel)是 Golang 实现 CSP 并发模型的关键,⿎励⽤通讯来实现数据共享。可以说,缺了 channel,goroutine 会黯然失⾊。

下面我将从源码角度解释Go语言对“通道”的抽象。


3be247cac2eb92713e93d3c64840ed48.png

1. 通道的创建

Go代码中,创建通道语句十分简单:

ch := make(chan type, int)

type是通道传输数据类型,int是通道容量。详情看我之前的文章吧。

如果我们在代码中调用了make就会生成一个通道。

源码1-chan的定义

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【数据项类型】
	sendx    uint   // send index
	recvx    uint   // receive index
	recvq    waitq  // list of recv waiters
	sendq    waitq  // list of send waiters
        【1】
	// 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
}

【1】lock mutex

很明显这是一个锁。并且类型mutex还是一个互斥锁。它保护hchan中的所有字段和处于sudog状态的、用到了此通道的goroutine中的一些字段。

ps:sudog是一个goroutine状态:表示等待列表中的g,例如在一个通道中用于发送/接收。

源码2-chan的创建

func makechan(t *chantype, size int) *hchan {
	【获得通道类型】
        elem := t.elem
        【检查数据项大小,不能超过64kb】
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
        【检查对齐】
	if hchanSize%maxAlign != 0 || elem.align > maxAlign {
		throw("makechan: bad alignment")
	}
        【1】
	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 *hchan
        【2】
	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)
        【如果设置了debug就输出chan信息】
	if debugChan {
		print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "n")
	}
	return c
}

【1】

对于【1】,是在检查内存是否溢出。其中函数MulUintptr表示两个参数的乘积,并判断是否溢出。其定义如下:

func MulUintptr(a, b uintptr) (uintptr, bool) {
	if a|b < 1<<(4*sys.PtrSize) || a == 0 {
		return a * b, false
	}
	overflow := b > MaxUintptr/a
	return a * b, overflow
}

a和b中其中一个不能大于65536,或者a(就是数据项目数量)等于0,那么返回 a*b和false。mem表示数据项*数据项大小,就是数据内存大小。

【2】

受到垃圾回收机制的限制,缓冲槽必须单独分配内存。

  1. mem为0就申请大小为 hchanSize 的空间,缓存指针即为chan;
  2. 如果元素不包含指针,就调整 buf 指针,chan + 数据内存占用;
  3. 其他就是正常的;

2. 数据的收发

2.1 涉及结构体

通道收发数据必须对 channel 双方进行封装,这涉及到goroutine的 g 结构体和 sudog 结构体中一些参数:

type g struct {
	param          unsafe.Pointer // passed parameter on wakeup
}

sudo中的注释也说了,以下某些字段受 hchan.lock 保护,他阻塞在channel中;并且堆栈的收缩也是取决于 channel 状态。

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
	elem     unsafe.Pointer // data element (may point to stack)
}

另外 channel 还的维护发送和接受者等待队列,以及异步缓冲槽环状队列索引。

type hchan struct {
	sendx    uint   // send index【发送槽位置索引】
	recvx    uint   // receive index【接收槽位置索引】
	recvq    waitq  // list of recv waiters【接收者等待队列】
	sendq    waitq  // list of send waiters【发送者等待队列】
}
type waitq struct {
	first *sudog
	last  *sudog
}

2.2 发

【发数据给chan】

// 调用下面语句,编译器将调用【chansend()】
chan <- x
// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
	chansend(c, elem, true, getcallerpc())
}

收发其实很easy啦。匹配到收发双方就行。然后就是熟练的运用阻塞挂起技能就行。

下面这个函数会由 chansend1 调用。

一句话总结:发送数据时候,会先在通道的接收队列中寻找接收者;没有就打包成为sudog存储在发送队列之中。

有人说相当于一个两级管理模式?你品,你细品。

c:通道指针,ep:参数指针,block:是否阻塞,callerpc:函数指针;返回 bool 类型。

/*
 * generic single channel send/recv
 * If block is not nil,
 * then the protocol will not
 * sleep but return if it could
 * not complete.
 *
 * sleep can wake up with g.param == nil
 * when a channel involved in the sleep has
 * been closed.  it is easiest to loop and re-run
 * the operation; we'll see that it's now closed.
 */
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, funcPC(chansend))
	}
        【1】
	// 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 c.recvq.first or c.qcount depending on kind of channel).
	// 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.
	if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
		(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
		return false
	}
        // 【这个 t0 不知道,好像是cpu打点计时器】
	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}
        // 【通道上锁】
	lock(&c.lock)
        // 【通道关闭,抛错】
	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}
        【2】
	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
	}
        【3】
	if c.qcount < c.dataqsiz {
		// Space is available in the channel buffer. Enqueue the element to send.
		qp := chanbuf(c, c.sendx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		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
	}
	【4】
	// 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)
	goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
	// 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)
	【5】
	// someone woke us up.
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if gp.param == nil {
		if c.closed == 0 {
			throw("chansend: spurious wakeup")
		}
		panic(plainError("send on closed channel"))
	}
	gp.param = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	mysg.c = nil
	releaseSudog(mysg)
	return true
}

【1】

对于这个判断的意思首先要明白,我们创建一个通道 c chan,可以定义它的容量,也就是 dataqsiz;默认是没有容量的,这个时候就会堵塞在这里,也就是 block 的值应该为 true;如果有容量,传输数据量也不应该大于它;他如果满了,也应该阻塞。所以这个判断的意思就是:如果:

  1. !block :通道不阻塞模式;
  2. c.closed ==0 :通道没关闭;
  3. (c.dataqsiz ==0&& c.recvq.first ==nil) 或者 (c.dataqsiz >0&& c.qcount == c.dataqsiz):容量为0并且没有接收其他数据,或者,容量有但是容量已满。

上述中如果全部出现,那么就错了,直接返回 False。

【2】

获得通道 c 的接受队列,然后调用 send函数;send函数基本上和本函数后面一样,就不多说了。

【3】判断(存量<容量)

数据送入缓存操作。

流程依此过一遍吧。

  • chanbuf (),返回位于 c.sendx 位置的指针;
  • raceenabled 用于运行go语言时,加 -race 命令跟踪程序数据;
  • typedmemmove(c.elemtype, qp, ep),将 ep 数据拷贝到 qp;
  • 后面就是存量自加,sendx 自加,解锁等一系列基本操作;

【4】

基于程序运行到这里,还没有返回,说明什么?

说明这个通道没有缓存,没有被接收,它在等待接收。

那么此时go语言是怎么做呢?

Go语言创建了一个 sudog,来替代原来的goroutine!这样一来,原来的goroutine便可以继续执行其他代码,而不用阻塞(如果你是这样设计代码的话),从而达到异步的效果。

流程依次过一遍吧。

  • 获得当前goroutine,gp;获得sudog(没有现存的就新建一个)mysg
  • releasetime 不知道啥东西;
  • 依此拷贝 ep,gp,c;
  • 将 mysg 加入 gp 的等待队列;
  • 将 mysg 加入 c 的发送等待队列;
  • goparkunlock () 阻塞等待被唤醒;
  • KeepAlive (ep) 用来给 ep 保活;
  • 等待唤醒;

【5】被唤醒

因为期间 sudog 可能被人用过,所以判断一下mysg 是否等于 gp.waiting;

进一步再判断一下是否是被 closechan () 唤醒;

最后,确认数据已经传输后,releaseSudog(mysg) 将sudog放回复用。

2.3 收

【从chan收数据】

// 调用下面语句,编译器将调用函数【chanrecv1()】
x <- chan

// 调用下面语句,编译器将调用函数【chanrecv2()】
x, bool <- chan

相比 chanrecv1() ,chanrecv2()比它多返回了一个 Bool 类型的 received,以判断是否正常接收数据。

// entry points for <- c from compiled code
//go:nosplit
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 源码哦。

// 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.
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.
        【1】
	if debugChan {
		print("chanrecv: chan=", c, "n")
	}

	if c == nil {
		if !block {
			return
		}
		gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
		throw("unreachable")
	}
        【2】
	// Fast path: check for failed non-blocking operation without acquiring the lock.
	//
	// After observing that the channel is not ready for receiving, we observe that the
	// channel is not closed. Each of these observations is a single word-sized read
	// (first c.sendq.first or c.qcount, and second c.closed).
	// 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.
	//
	// The order of operations is important here: reversing the operations can lead to
	// incorrect behavior when racing with a close.
	if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
		c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
		atomic.Load(&c.closed) == 0 {
		return
	}

	var t0 int64
	if blockprofilerate > 0 {
		t0 = cputicks()
	}

	lock(&c.lock)
        【3】
	if c.closed != 0 && c.qcount == 0 {
		if raceenabled {
			raceacquire(c.raceaddr())
		}
		unlock(&c.lock)
		if ep != nil {
			typedmemclr(c.elemtype, ep)
		}
		return true, false
	}
        【4】
	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
	}
        【5】
	if c.qcount > 0 {
		// Receive directly from queue
		qp := chanbuf(c, c.recvx)
		if raceenabled {
			raceacquire(qp)
			racerelease(qp)
		}
		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
	}
        【6】
	// no sender available: block on this channel.
	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
	gp.waiting = mysg
	mysg.g = gp
	mysg.isSelect = false
	mysg.c = c
	gp.param = nil
	c.recvq.enqueue(mysg)
	goparkunlock(&c.lock, waitReasonChanReceive, traceEvGoBlockRecv, 3)
        【7】
	// someone woke us up
	if mysg != gp.waiting {
		throw("G waiting list is corrupted")
	}
	gp.waiting = nil
	if mysg.releasetime > 0 {
		blockevent(mysg.releasetime-t0, 2)
	}
	closed := gp.param == nil
	gp.param = nil
	mysg.c = nil
	releaseSudog(mysg)
	return true, !closed
}

【1、2、4、5、6】

对于这些代码部分,其实可以完全参照“发送”部分代码的解释;

【3】

对于【3】我想解释的是,c.qcount 是代表此时通道 c 中有多少个数据;发送和接收都用到了它,仔细看。如果 qcount 等于0,说明没有数据;那还接收个毛线~~~

【7】

对于【7】,我想解释一下 gp.param:这个参数负责唤醒,接收完成,他应该等于 nil;

3. 关闭

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"))
	}
        // 【加入 race 路径】
	if raceenabled {
		callerpc := getcallerpc()
		racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
		racerelease(c.raceaddr())
	}
        // 【设置 closed 为 True】
	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 = nil
		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 = nil
		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)
	}
}

关闭通道的代码解释写在上面~~还挺简单的,不多说了。


END

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值