大纲
文章目录
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
对应函数分别是 chanrecv1
和 chanrecv2
,位于 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)
}
- 如果是不带 buffer 的 channel ,那么只需要分配出一个 hchan 结构体即可;
- 如果 channel 元素(elem)内不含指针,那么 hchan 和 buffer 其实是可以在一起分配的,hchan 和 elem buffer 的内存块连续;
- 如果 channel 元素(elem)是带有指针的,那么 hchan 和 buffer 就不能分配在一起,所以先 new 一个 hchan 结构,再单独分配 elem buffer 数组;
所以我们看到除了 hchan 结构体本身的内存分配,该结构体初始化的关键在于四个字段:
// channel 的元素 buffer 数组地址;
c.buf = mallocgc(mem, elem, true)
// channel 元素大小,如果是 int,那么就是 8 字节;
c