GoLang之channel底层的数据结构是什么、channel的创建(2)

GoLang之channel底层的数据结构是什么、channel的创建(2)

1.数据结构

底层数据结构需要看源码,版本为 go 1.9.2:

// hchan结构体位于runtime/chan.go文件
type hchan struct {
	// chan 里元素数量
	qcount   uint
	// chan 底层循环数组的长度
	dataqsiz uint
	// 指向底层循环数组的指针
	// 只针对有缓冲的 channel
	buf      unsafe.Pointer
	// chan 中元素大小;chan 中每个元素的大小,用于在buf中定位元素位置
	elemsize uint16
	// chan 是否被关闭的标志
	closed   uint32
	// chan 中元素类型,用于数据传递过程中的赋值
	elemtype *_type // element type
	// 已发送元素在循环数组中的索引
	sendx    uint   // send index
	// 已接收元素在循环数组中的索引
	recvx    uint   // receive index
	// 等待接收的 goroutine 队列
	recvq    waitq  // list of recv waiters
	// 等待发送的 goroutine 队列
	sendq    waitq  // list of send waiters

	// 保护 hchan 中所有字段
	lock mutex
}

关于字段的含义都写在注释里了,再来重点说几个字段:

buf 指向底层循环数组,只有缓冲型的 channel 才有。

sendxrecvx 均指向底层循环数组,表示当前可以发送和接收的元素位置索引值(相对于底层数组)。

sendqrecvq 分别表示被阻塞的 goroutine,这些 goroutine 由于尝试读取 channel 或向 channel 发送数据而被阻塞。

waitqsudog 的一个双向链表,该结构中存储了两个分别指向前后 runtime.sudog 的指针以构成链表。而 sudog 实际上是对 goroutine 的一个封装,表示一个在等待列表中的 goroutine:

// waitq结构体位于runtime/chan.go文件
type waitq struct {
	first *sudog
	last  *sudog
}

lock 用来保证每个读 channel 或写 channel 的操作都是原子的。

例如,创建一个容量为 6 的,元素为 int 型的 channel 数据结构如下 :

chan data structure

channel中有两个队列,一个是发送阻塞队列,一个是接收阻塞队列。当向一个已满的channel发送数据会被阻塞,此时发送协程会被添加到sendq中,同理,当向一个空的channel接收数据时,接收协程也会被阻塞,被置入recvq中
总结hchan结构体的主要组成部分有四个:
1.用来保存goroutine之间传递数据的循环链表。=> buf。
2.用来记录此循环链表当前发送或接收数据的下标值。
=> sendx和recvx。
3.用于保存向该chan发送和从该chan接收数据的goroutine的队列。=====> sendq 和 recvq
4.保证channel写入和读取数据时线程安全的锁。 =====> lock

在这里插入图片描述

2.创建

我们知道,通道有两个方向,发送和接收。理论上来说,我们可以创建一个只发送或只接收的通道,但是这种通道创建出来后,怎么使用呢?一个只能发的通道,怎么接收呢?同样,一个只能收的通道,如何向其发送数据呢?

一般而言,使用 make 创建一个能收能发的通道:

// 无缓冲通道
ch1 := make(chan int)
// 有缓冲通道
ch2 := make(chan int, 10)

通过汇编分析,我们知道,最终创建 chan 的函数是 makechan

func makechan(t *chantype, size int64) *hchan

从函数原型来看,创建的 chan 是一个指针。所以我们能在函数间直接传递 channel,而不用传递 channel 的指针。

具体来看下代码:

// makechan函数位于runtime/chan.go文件
const hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))

func makechan(t *chantype, size int64) *hchan {
	elem := t.elem

	// 省略了检查 channel size,align 的代码
	// ……

	var c *hchan
	// 如果元素类型不含指针 或者 size 大小为 0(无缓冲类型)
	// 只进行一次内存分配
	if elem.kind&kindNoPointers != 0 || size == 0 {
		// 如果 hchan 结构体中不含指针,GC 就不会扫描 chan 中的元素
		// 只分配 "hchan 结构体大小 + 元素大小*个数" 的内存
		c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
		// 如果是缓冲型 channel 且元素大小不等于 0(大小等于 0的元素类型:struct{})
		if size > 0 && elem.size != 0 {
			c.buf = add(unsafe.Pointer(c), hchanSize)
		} else {
			// race detector uses this location for synchronization
			// Also prevents us from pointing beyond the allocation (see issue 9401).
			// 1. 非缓冲型的,buf 没用,直接指向 chan 起始地址处
			// 2. 缓冲型的,能进入到这里,说明元素无指针且元素类型为 struct{},也无影响
			// 因为只会用到接收和发送游标,不会真正拷贝东西到 c.buf 处(这会覆盖 chan的内容)
			c.buf = unsafe.Pointer(c)
		}
	} else {
		// 进行两次内存分配操作
		c = new(hchan)
		c.buf = newarray(elem, int(size))
	}
	c.elemsize = uint16(elem.size)
	c.elemtype = elem
	// 循环数组长度
	c.dataqsiz = uint(size)

	// 返回 hchan 指针
	return c
}

新建一个 chan 后,内存在堆上分配,大概长这样:

make chan

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值