golang channel 最详细的源码剖析

本文详细剖析了Golang的channel,从基本概念到使用方式,包括创建、入队、出队以及结合select和for-range的用法。通过对源码的解读,阐述了channel的核心结构hchan及其关键函数如makechan、chansend和chanrecv的工作原理,帮助读者深入理解channel的内部运作机制。
摘要由CSDN通过智能技术生成

大纲


chan 是 golang 的最重要的一个结构,是区别于其他高级语言的最重要的特色之一,也是 goroutine 通信必须要的要素之一。很多人用它,但是很少人彻底理解过它,甚至 c <- x<-c 这样的语法可能都记不清晰,怎么办?本文教你从源码编译器的角度全方位的剖析 channel 的用法。

channel 是什么?

本质上就实现角度来讲,golang 的 channel 就是一个环形队列(ringbuffer)的实现。我们称 chan 为管理结构,channel 里面可以放任何类型的对象,我们称之为元素。

我们从 channel 的使用姿势入手,讲解最详细的 channel 使用方法。
在这里插入图片描述

channel 使用姿势

我们从宏观的 chan 使用姿势入手,总结来讲,有以下几种姿势:

  • chan 的创建
  • chan 入队
  • chan 出队
  • select 和 chan 结合
  • for-range 和 chan 结合

chan 创建

创建一个 channel ,一般用户使用姿势有两种,分别是创建有 buffer 和没有 buffer 的 channel 。

// no buffer 的 channel
c := make(chan int)
// 自带 buffer 的 channel 
c1 := make(chan int , 10)

这个对应了实际函数是 makechan ,位于 runtime/chan.go 文件里。

chan 入队

用户使用姿势:

c <- x

对应函数实现 chansend ,位于 runtime/chan.go 文件。

chan 出队

用户使用姿势:

v := <-c
v, ok := <-c

对应函数分别是 chanrecv1chanrecv2 ,位于 runtime/chan.go 文件。

结合 select 语句

用户使用姿势:

select {
   
case c <- v:
	//  ... foo
default:
	//  ... bar
}

对应函数实现为 selectnbsend , 位于 runtime/chan.go 文件中。

用户使用姿势:

select {
   
case v = <-c:
	//  ... foo
default:
	//  ... bar
}

对应函数实现为 selectnbrecv , 位于 runtime/chan.go 文件中。

用户使用姿势:

select {
   
case v, ok = <-c:
	//  ... foo
default:
	//  ... bar
}

对应函数实现为 selectnbrecv2 , 位于 runtime/chan.go 文件中。

结合 for-range 语句

用户使用姿势:

for m := range c {
   
    // ...   do something
}

对应使用函数 chanrecv2 ,位于 runtime/chan.go 文件中。

源码解析

上面我们通过宏观的用户使用姿势,了解清楚了不同的使用姿势对应了不同实现函数(这个翻译是编译器来做的),我们接下来就是仔细看下这些函数的实现。

makechan

负责 channel 的创建,当我们 go 程序里写类似 v := make(chan int) 的初始化语句,就会相应的调用不同类型对应的初始化函数,其中 channel 的初始化函数就是 makechen

runtime.makechan

定义原型:

func makechan(t *chantype, size int) *hchan {
   
}

通过这个,我们能得知到,声明创建一个 channel ,本质上是得到了一个 hchan 的指针,所以 channel 的核心结构就是基于 hchan 来实现的。

其中 t 参数是指明元素类型:

type chantype struct {
   
	typ  _type
	elem *_type
	dir  uintptr
}

size 指明这个 channel buffer 槽位有多少。如果是带 buffer 的 channel,比如那么 size 就是槽位数,如果没有指定,那么就是 0;

// size == 0
a := make(chan int)
// size == 2
b := make(chan int, 2)

我们看下 makechan 做的事情,其实很简单,就只做了两件事:

func makechan(t *chantype, size int) *hchan {
   
    // 参数校验
    // 初始化 hchan 结构
}

参数校验无非就是一些越界,或者 limit 的校验。

初始化 hchan 则简单的分为三种情况:

switch {
   
// no buffer 的场景,这种 channel 可以看成 pipe;
case mem == 0:
    c = (*hchan)(mallocgc(hchanSize, nil, true))
    c.buf = c.raceaddr()
// channel 元素不含指针的场景,那么是分配出一个大内存块;
case elem.ptrdata == 0:
    c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
    c.buf = add(unsafe.Pointer(c), hchanSize)
// 默认场景,hchan 结构体和 buffer 内存块单独分配;
default:
    c = new(hchan)
    c.buf = mallocgc(mem, elem, true)
}
  1. 如果是不带 buffer 的 channel ,那么只需要分配出一个 hchan 结构体即可;
  2. 如果 channel 元素(elem)内不含指针,那么 hchan 和 buffer 其实是可以在一起分配的,hchan 和 elem buffer 的内存块连续;
  3. 如果 channel 元素(elem)是带有指针的,那么 hchan 和 buffer 就不能分配在一起,所以先 new 一个 hchan 结构,再单独分配 elem buffer 数组;

所以我们看到除了 hchan 结构体本身的内存分配,该结构体初始化的关键在于四个字段:

// channel 的元素 buffer 数组地址;
c.buf = mallocgc(mem, elem, true)
// channel 元素大小,如果是 int,那么就是 8 字节;
c.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值