Go channel——基本机制及结构

前言

GO官方博客Share Memory By Communicating中提到:

Go’s concurrency primitives - goroutines and channels - provide an elegant and distinct means of structuring concurrent software. (These concepts have an interesting history that begins with C. A. R. Hoare’s Communicating Sequential Processes.) Instead of explicitly using locks to mediate access to shared data, Go encourages the use of channels to pass references to data between goroutines. This approach ensures that only one goroutine has access to the data at a given time. The concept is summarized in the document Effective Go (a must-read for any Go programmer):

Do not communicate by sharing memory; instead, share memory by communicating.

Go原生提供goroutinechannel以优雅构建并发软件,并且鼓励通过channel来传递数据的引用。然后提到了堪称Go并发的准则:

不要通过共享内存来通信,相反,要通过通信来共享内存。

此口号在effective go Concurrency亦有提及。

这个通信的工具自然就是channel,由此可见channel的重要性。

关于channel的介绍明将通过一个小系列的文章说明,本文主要介绍channel的结构及make、close的处理。

更多内容分享,欢迎关注公众号:Go开发笔记

channel

channel的底层实现位于runtime/chan.go文件中,以下是官方在文件开头处的注释说明。

// This file contains the implementation of Go channels.

// Invariants:
//  At least one of c.sendq and c.recvq is empty,
//  except for the case of an unbuffered channel with a single goroutine
//  blocked on it for both sending and receiving using a select statement,
//  in which case the length of c.sendq and c.recvq is limited only by the
//  size of the select statement.
//
// For buffered channels, also:
//  c.qcount > 0 implies that c.recvq is empty.
//  c.qcount < c.dataqsiz implies that c.sendq is empty.

翻译过来,大致意思是:

此文件包含Go channel的实现。

不变量:

c.sendq(发送等待队列)和c.recvq(接收等待队列)中至少有一个是空的,除了使用select语句为发送和接收而阻塞了单个goroutine的无缓存通道外,在这种情况下,c.sendq和c.recvq的长度仅受select语句大小的限制。

对于缓存通道,也包括:

c.qcount>0表示接收等待队列c.recvq为空。

c.qcount<c.dataqsiz表示发送等待队列c.sendq为空。

关于c.sendq(发送等待队列)和c.recvq(接收等待队列)中至于有一个是空的,可以这么理解:

为保证chan内数据的队列性(FIFO)

  • 发送时,优先级为:直接发送>缓存(有等待的接收者时,优先直接发送至等待的接收者,否则缓存,缓存满后则加入发送等待队列)

  • 接收时,优先级为:缓存>直接接收(有缓存时,优先接收缓存,否则若有等待的发送者则直接从等待的发送者接收数据,否则加入接收等待队列)

因此,当有缓存时,说明已没有等待的接收者,接收等待队列为空;而当缓存未满时,数据会继续存在缓存中,而不会加入发送等待队列,发送等待队列为空

简易的处理机制可以绘制如下图所示:

在这里插入图片描述

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
}

channel底层是一个struct结构的hchan,其内部参数大致解释如下。

  • dataqsiz:channel缓存容量,能容纳的最大缓存。
  • qcount:当前缓存大小。
    • 初始化时为0。
    • 无缓存的channelqcount一直为0。
    • 对于有缓存的channel,当没有等待的接收者时,qcount增加,qcount最大为dataqsiz;没有等待的发送者时,qcount减少,最小为0。
  • elemtype: channel类型
  • elemsize: channel类型的elem的大小
  • buf: 指向缓存数组
  • sendx: 待发送缓存的索引,用以存入buf
  • recvx: 待接收缓存的索引,用以从buf中取出缓存
  • sendq:发送等待队列,当发送数据没有接收者接收,且缓存不足时,会加入发送等待队列
  • recvq:接收等待队列,当接收者没有数据接收,会加入发送等待队列
  • closed: 1为关闭,0为未关闭
  • lock: 内部锁

makechan

channel的make方式如下:

make(chan typ,size)

其内部实现为makechan:

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

对于数据结构的初始化最终都是对内存地址的分配,channelmake过程亦是如此。

  1. 对chantype的elem的size及align进行校验
  2. 根据elem的size及数量计算存储这些elem的内存大小,校验是否内存溢出
  3. 分配内存,构造*hchan
    • 对于无缓存channel,缓存占用的内存为0,直接分配chan自身的大小内存,buf则取自身地址。
    • 对于类型中不包含指针的channel,一起分配内存,直接分配chan自身的大小+缓存大小的内存,buf指向chan后的缓存地址。
    • 对于类型中包含指针的channel,分开分配内存,先new channel,然后单独分配缓存的内存。
  4. 初始化hchan的参数,chane类型大小的elemsize,类型的elemtype,缓存队列大小的dataqsiz。

需要注意的是:

  • channel的缓存大小是有限制的,最大为65536
  • channel分配内存也是有限制的

总结

本文主要重点如下:

  • channel的基本机制

    c.sendq(发送等待队列)和c.recvq(接收等待队列)中至于有一个是空的

    • 发送时,优先级为:直接发送>缓存(有等待的接收者时,优先直接发送至等待的接收者,否则缓存,缓存满后则加入发送等待队列)

    • 接收时,优先级为:缓存>直接接收(有缓存时,优先接收缓存,否则若有等待的发送者则直接从等待的发送者接收数据,否则加入接收等待队列)

  • hchanchannel的底层结构

  • make过程是内存的计算、分配过程,并初始化了channel的类型、缓存容量参数。

下一篇文章将会关于channel的核心,发送与接收。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值