Golang语言系列-Channel


golang里的channel信道是golang里一个独特的概念,基于消息通信的方式来实现并发控制。信道有两种类型,缓存型和非缓存型,其中缓冲型底层基于循环数组来保存数据,然后基于互斥锁保证并发访问安全。对于信道可以有三种操作,分别是读,写以及关闭。读一个nil信道,当前协程会被挂起,读一个已经关闭的信道,如果信道中有元素可以正常读取,如果没有会读取对应类型的零值。如果信道是非缓冲的,等待发送队列中有协程,直接从对应协程中拷贝数据,否则或者如果缓冲为空当前协程阻塞,加入到信道的等待发送队列中。则从缓冲头部中读取一个数据,写一个nil信道,同样会被挂起,写一个已经关闭的信道,会panic,写的时候,也是类似,如果信道的等待接收队列中有协程,直接将数据拷贝过去,否则将当前协程阻塞,加入到等待发送队列。如果关闭一个nil或者已经关闭的信道,也会panic。
本文将从源码分析的角度验证以上的观点。最后实现golang的信道来实现一个多线程打印问题。

源码分析

channel相关的代码在runtime包下的chan.go文件中。

结构体定义和构造函数

首先关注信道的结构体定义,源码如下:

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    // 并发访问的互斥锁
}

发送等待或者接收等待的waitq是一个链表,链表上面的每一个节点是一个指向包装go协程的sudog,其结构体定义如下:

type waitq struct {
   
      // 用于保存阻塞在信道上的协程的双向链表
	first *sudog
	last  *sudog
}
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   // 指向被阻塞的协程

	next *sudog  // 链表上的下一个
	prev *sudog   // 链表上的上一个
	elem unsafe.Pointer // data element (may point to stack)

	// The following fields are never accessed concurrently.
	// For channels, waitlink is only accessed by g.
	// For semaphores, all fields (including the ones above)
	// are only accessed when holding a semaRoot lock.

	acquiretime int64
	releasetime int64
	ticket      uint32

	// isSelect indicates g is participating in a select, so
	// g.selectDone must be CAS'd to win the wake-up race.
	isSelect bool

	// success indicates whether communication over channel c
	// succeeded. It is true if the goroutine was awoken because a
	// value was delivered over channel c, and false if awoken
	// because c was closed.
	success bool

	parent   *sudog // semaRoot binary tree
	waitlink *sudog // g.waiting list or semaRoot
	waittail *sudog // semaRoot
	c        *hchan // channel
}

最后来看一下信道的构造函数,源代码如下:

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

	// 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.
		// 无缓冲或者元素大小为零分配的96个字节
		c = (*hchan)(mallocgc(hchanSize, nil, true))
		// Race detector uses this location for synchronization.
		c.buf = c.raceaddr()
	case elem.PtrBytes == 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
}

发送操作

所有相关的源码如下:

// chansend 为通用的信道发送函数,实际上我们使用的 c <- x, 经过编译调用的是chansend1函数,其又会调用chansend函数,而传入的函数block是true,也就是说要阻塞,但这个调用的信道发送函数可以实现当无法发送的时候可以不阻塞 
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
   
   
	if c == nil {
   
      // 信道为nil
		if !block {
   
      // 不阻塞,返回
			return false
		}
		gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)   // 阻塞模式下,panic
		throw("unreachable")
	}

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

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

	// 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 full()).
	// 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. However, nothing here
	// guarantees forward progress. We rely on the side effects of lock release in
	// chanrecv() and closechan() to update this thread's view of c.closed and full().
	// 非阻塞模式下,且信道未关闭并且无法发送数据(缓冲队列已满或者等待接收队列为空),此时直接返回false,走捷径返回,避免加锁的开销
	if !block && c.closed == 0 && fu
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值