【go源码分析】go源码之channel源码分析

    Channel 是支撑 Go 语言高性能并发编程模型的重要结构,Channel 是一个用于同步和通信的有锁队列,使用互斥锁解决程序中可能存在的线程竞争问题。源码路径runtime/chan.go

CSP 模型

     CSP 模型全称为 communicating sequential processes,CSP 模型由并发执行实体(进程,线程或协程),和消息通道组成,实体之间通过消息通道发送消息

channel读写

ch := make(chan int)

// write
ch <- x

// read
x <- ch

// another read
x = <- ch

  注意: channel 初始化后才能进行读写操作,否则阻塞

关闭 channel

ch := make(chan int)

close(ch)
  • 关闭未初始化 channel 会产生 panic
  • 重复关闭 channel 产生 panic
  • 向已关闭 channel 中发送会产生 panic
  • 从已关闭的 channel 读取不会 panic,能读出 channel 未被读取的,若消息均已读出则会读到类型的零值。从已关闭的 channel 中读取不会阻塞,会返回一个为 false 的 ok-idiom(判断 channel 是否关闭)
  • 关闭 channel 产生广播机制,所有读取channel的 goroutine 都会收到消息

0. 结构体

    hchan结构体,Channel 实际上是个环形队列。

  •   qcount — Channel 中的元素个数
  •   dataqsiz — Channel 中的循环队列的大小
  •   buf — Channel 的缓冲区数据指针
  •   sendx — Channel 的发送操作的位置
  •   recvx — Channel 的接收操作的位置
  •   elemsize — 当前 Channel 能够收发的元素类型
  •   elemtype — Channel 能够收发的元素大小
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 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
}

      sendq 和 recvq 表示 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列表,使用双向链表 runtime.waitq 表示,元素都是 runtime.sudog 结构 

type waitq struct {
	first *sudog
	last  *sudog
}

1.  创建管道 — makechan函数

   Channel 的创建都会使用 make 关键字,如果不向 make 传递缓冲区大小的参数,那么就会设置一个默认值 0,也就是当前的 Channel 不存在缓冲区。

case TCHAN:
	l = nil
	if i < len(args) {
		l = args[i]
		i++
		l = typecheck(l, ctxExpr)
		l = defaultlit(l, types.Types[TINT])
		if l.Type == nil {
			n.Type = nil
			return n
		}
		if !checkmake(t, "buffer", &l) {
			n.Type = nil
			return n
		}
		n.Left = l
	} else {
		n.Left = nodintconst(0)
	}
	n.Op = OMAKECHAN
}

    调用 runtime.makechan 或者 runtime.makechan64 的函数,makechan64 处理缓冲区大小大于 2 的 32 次方的情况,重点关注 runtime.makechan 函数

case OMAKECHAN:
	// When size fits into int, use makechan instead of
	// makechan64, which is faster and shorter on 32 bit platforms.
	size := n.Left
	fnname := "makechan64"
	argtype := types.Types[TINT64]

	// Type checking guarantees that TIDEAL size is positive and fits in an int.
	// The case of size overflow when converting TUINT or TUINTPTR to TINT
	// will be handled by the negative range checks in makechan during runtime.
	if size.Type.IsKind(TIDEAL) || maxintval[size.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
		fnname = "makechan"
		argtype = types.Types[TINT]
	}

	n = mkcall1(chanfn(fnname, 1, n.Type), n.Type, init, typename(n.Type), conv(size, argtype))

    1.1 合法性验证

  •    数据类型大小,大于1<<16时异常
  •    内存对齐(降低寻址次数),大于最大的内存8字节数时异常
  •    传入的size大小,大于堆可分配的最大内存时异常
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"))
	}

    1.2 分配地址空间

     根据 channel 中收发元素的类型和缓冲区的大小初始化 runtime.hchan 和缓冲区

  •    如果 channel 不存在缓冲区,分配 hchan 结构体空间,即无缓存 channel
  •    如果 channel 存储的类型不是指针类型,分配连续地址空间,包括 hchan 结构体 + 数据
  •    默认情况包括指针,为 hchan 和 buf 单独分配数据地址空间
	// 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
	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)
	}

    更新 hchan 结构体的数据,包括 elemsize elemtype 和 dataqsiz

	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	c.dataqsiz = uint(size)
	lockInit(&c.lock, lockRankHchan)

2. chansend函数

  操作如: ch <- x。 编译器解析成 OSEND 并在 cmd/compile/internal/gc/walk.go 转换成 chansend1 函数。

	case OSEND:
		n1 := n.Right
		n1 = assignconv(n1, n.Left.Type.Elem(), "chan send")
		n1 = walkexpr(n1, init)
		n1 = nod(OADDR, n1, nil)
		n = mkcall1(chanfn("chansend1", 2, n.Left.Type), nil, init, n.Left, n1)
// entry point for c <- x from compiled code
//go:nosplit
func chansend1(c *hchan, elem unsafe.Pointer) {
        chansend(c, elem, true, getcallerpc())
}

   

    chansend 函数内容相当多,可以归纳为三部分:

  • 当存在等待的接收者时,也就是在 recvq 可以获得 waitq,通过 send 方法直接将数据发送给等待的接收者
  • 当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区
  • 当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据
/*
 * 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 {

  未创始化为nil,向其中发送数据将会阻塞

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

  这是队列满的情况

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

  已经关闭channel,向其中发送将panic

	if c.closed != 0 {
		unlock(&c.lock)
		panic(plainError("send on closed channel"))
	}

  2.1 chansend 主要内容一

    当存在等待的接收者时,也就是在 recvq 可以获得 waitq,通过 send 方法直接将数据发送给等待的接收者(第3节讲解)方法

    goroutine 阻塞在 channel 上,直接将数据发送给该 goroutine,从当前 channel 的等待队列中取出等待的 goroutine,然后调用 send。

	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
	}

  2.2 chansend 主要内容二

     当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区

     如果创建的 Channel 包含缓冲区并且 Channel 中的数据没有装满,typedmemmove 将发送的数据拷贝到缓冲区中并增加 sendx 索引和 qcount 计数器

	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
	}

  2.3 chansend 主要内容三 阻塞发送

  1. getg 获取发送数据的 Goroutine
  2. acquireSudog 获取 sudog 结构
  3. 将创建并初始化的 sudog 加入发送等待队列,并设置到当前 Goroutine 的 waiting上,表示 Goroutine 正在等待该 sudog 准备就绪
  4. gopark 将当前的 Goroutine 陷入沉睡等待唤醒
  5. 被调度器唤醒后会将一些属性置零并且释放 runtime.sudog 结构体
	// 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)

	// 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

3. send函数

  • 调用 sendDirect 将发送的数据直接拷贝到接收方 sg 中
  • 调用 goready 将等待接收数据的 Goroutine 标记成可运行状态 Grunnable, 并把该 Goroutine 放到发送方所在的处理器的 runnext 上,该处理器调度时会唤醒接收方
// 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.
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
	}
	gp := sg.g
	unlockf()
	gp.param = unsafe.Pointer(sg)
	sg.success = true
	if sg.releasetime != 0 {
		sg.releasetime = cputicks()
	}
	goready(gp, skip+1)
}

4. closechan函数

  4.1 close未初始化channel产生panic,关闭已经closed的channel将产生panic

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

  4.2 释放所有读取

	// 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())
		}
		gp.schedlink.set(glist)
		glist = gp
	}

  4.3 释放所有写

	// 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())
		}
		gp.schedlink.set(glist)
		glist = gp
	}

  4.4

	// Ready all Gs now that we've dropped the channel lock.
	for glist != nil {
		gp := glist
		glist = glist.schedlink.ptr()
		gp.schedlink = 0
		goready(gp, 3)
	}

5. chanrecv函数 

  x :=  <-ch

  5.1 未初始化channel,阻塞

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

  5.2 从已经关闭的且书记已经都读出,则返回数据类型的0值

	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
	}

  5.3 当前有发送 goroutine 阻塞在 channel 上,说明buf 已经满员

    

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

Go语言中的channel实现是通过一个称为"管道"(pipe)的数据结构来实现的。在go/src/runtime/chan.go文件中,定义了channel的数据结构: ```go type hchan struct { qcount uint // 队列中的元素数量 dataqsiz uint // 环形缓冲区大小 buf unsafe.Pointer // 指向环形缓冲区的指针 elemsize uint16 // 元素大小 closed uint32 // 是否已关闭 elemtype *_type // 元素类型 sendx uint // 发送位置 recvx uint // 接收位置 recvq waitq // 接收队列 sendq waitq // 发送队列 lock mutex // 互斥锁 } ``` 其中,qcount记录了当前channel中的元素个数,dataqsiz表示channel的缓冲区大小,buf指向channel的环形缓冲区,elemsize表示一个元素的大小,closed表示channel是否已关闭,elemtype表示channel中元素的类型,sendx表示下一个写入位置,recvx表示下一个读取位置,recvq和sendq分别表示等待读取和等待写入的goroutine队列,lock是保护channel的互斥锁。 在channel的实现中,有以下几个重要的函数: - chanmake:创建一个新的channel。 - chanclose:关闭一个channel。 - chansend:向一个channel中写入数据。 - chanrecv:从一个channel中读取数据。 - chanselect:可以同时等待多个channel的读写操作。 Go语言中的channel实现是基于CSP(Communicating sequential processes)模型的,通过channel进行协程之间的通信,实现了协程之间的同步。在底层实现中,channel通过互斥锁和条件变量来保证并发安全。当有多个goroutine同时读写channel时,会使用等待队列来避免busy waiting,提高了并发效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值