golang channel 最详细的源码剖析

大纲

文章目录

    • channel 是什么?
    • channel 使用姿势
      • chan 创建
      • chan 入队
      • chan 出队
      • 结合 select 语句
      • 结合 for-range 语句
    • 源码解析
      • `makechan`
      • hchan 结构
      • chansend
      • chanrecv
      • selectnbsend
      • selectnbrecv
      • selectnbrecv2
      • chanrecv2
    • 总结

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

channel 是什么?

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

我们从 channel 的使用姿势入手,讲解最详细的 channel 使用方法。
golang channel 最详细的源码剖析_第1张图片

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)

你可能感兴趣的:(最细节篇,golang,源码分析,golang,channel,源码剖析,原理)