本文主要围绕channel的一些细节上做些总结和分享。主要涉及到两个方面一个是初始化,另外一个就是在数据实际接收和发送的时候的一些细节;这两个离不开golang编译器的协助,所以在了解go源码时,go编译器其实在其中起到了很大的作用。
makechanmake 方法 主要是初始化 切片、map和channel
这里主要说make(chan type)
例如:make(chan int)
通过编译其实解析出来就是 makechan方法 *makechan(t chantype, size int)
所以对应的:
ex.
// 有缓冲
make(chan int,2) => makechan(*chantype{
// int 有关的 chantype内容
}, 2)
//无缓冲
make(chan int) => makechan(*chantype{
// int 有关的 chantype内容
}, 0)
而基于chantype的内容,这里依据上文只放出int类型相关的chanType的内容:
(ps. 其中,elem中的ptrdata是指如果chan类型是个指针类型的管道,那么ptrdata就会有值,是其指针的大小;
这个ptrdata很重要,因为在后面为hchan申请空间的时候会拿来做判断)
在针对这次初始化,makechan中会根据chantype的不同内容来做不同的内存申请,具体可以看makechan方法的其中一段代码:
// src/runtime/chan.go:71
switch {
case mem == 0:
/*这个mem指的是根据elem的size(单类型字节数,这里int是八个字节),乘以size(我们这里size是2)得出的实际要申请的空间,
如果mem为0那肯定是size为0啦,就是无缓冲区那种,那么我们此时只需要申请一个hchan空间大小的连续内存即可*/
// 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:
/*如果ptrdata为0且mem不为0(上面mem没进入呀),表示此时我们make的chantype不是一个指针,那么此时申请内存就是hchan本身的再加mem的大小,足够用,且不会多浪费。
(如果 channel 元素(elem)内不含指针,那么 hchan 和 buffer 其实是可以在一起分配的,hchan 和 elem buffer 的内存块连续)*/
// 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:
/*如果以上,都没进入,那么首先,可以理解mem不为0,是有缓冲区channel,其次传递的是指针类型的,至于申请方式 参考资料里说:
(如果 channel 元素(elem)是带有指针的,那么 hchan 和 buffer 就不能分配在一起,所以先 new 一个 hchan 结构,再单独分配 elem buffer 数组;)*/
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
至于后面发送和接收的机制,后面再出个文章详细聊聊,到时候我会将其链接贴在这里。
chanRecv(chanSend)除了上述的分享外,还有一个点,就是channel相关语句被golang编译器解析后的使用方式
比如:
v<-chanElem
顾名思义,就是通过v变量来接收chanElem传递来的数据。但是其深层的含义是如何呢,这就涉及到chanrecv的方法了
其中有两个方法
//src/runtime/chan.go:405
chanrecv1(c *hchan, elem unsafe.Pointer)
//src/runtime/chan.go:410
chanrecv2(c *hchan, elem unsafe.Pointer) (received bool)
//没有面向对象重载,所以只能强行按照1、2来区分
他们内部共同调用的都是
// src/runtime/chan.go:421
chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
同样,这里不再解读chanrecv相关的逻辑,后面再出个文章详细聊聊;
主要说下什么时候 用chanrecv1,什么时候用chanrecv2;
首先他们的不同在于返回值,一个有返回值一个没有,
然后他们的共同点在于都是调用的chanrecv且形参block都传入的值是true。
这个block其实大有学问,这个后面我们细讲,单说这两个方法,其实就是go编译器根据不同的使用场景编译成不同的方法chanrecv1和chanrecv2来用的。
比如
//单纯的 :
v<-chanElem
//这里是不需要判断返回值的,那么编译过来就是 chanrecv1(chanElem, v)
//抑或:
<-chanElem(chanElem, nil)
//如果是
if v,ok:=<-chanElem;ok{
….
}
//则是
if ok:= chanrecv2(chanElem, v) ;ok{
….
}
他们有个共同点,都会block (block都为true,至于为何block 我们等会去到chanrecv源码里一看究竟)。
问题来了,block到底是来做什么的?其作用是啥呢?
可以看到,当我们的hchan为nil的时候 如果block为true,则当前协程会整个block。
且同样,我们的缓冲区无数据,没有数据可以接收的时候,当切协程也会block(图中没截出来无数据的时候block的逻辑,后续在接下来的文章中引申)。
-
问题:但是如果block为false,会怎样呢?
首先 当hchan为nil的时候,会即刻返回false,
当没数据的时候也会即刻返回false。 -
继续深问什么场景下会需要即刻返回呢?
有一个很经典的场景:select{}。
有时候针对channel,我们一般都会这么用:
select {
case v = <-c:
// ... foo
default:
// ... bar
}
这种场景一般用在 我们不断的循环,去从channel中获取数据,没有就继续循环。
而这里就有一个小知识也是考验基础的地方:
- 问题:既然channel拿不到数据阻塞,那么在select里面是否也是阻塞的呢?
会有很多人认为,select这里的case是阻塞,抱歉这里不能阻塞;
大家一想就能想清楚,首先当前协程要是在case1里被阻塞了,那我接下来的 case2,case3 还要不要判断了;
所以这里肯定是不能阻塞的,也是能从侧面来验证我们在使用channel的时候有没有去深思过channel的机制:
所以编译后,我们会看到这里的v<-c 其实用的是selectnbrecv2方法,而传参中有一个重要的参数:block 传入的是false,
// /usr/local/go/src/runtime/chan.go:657
func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
// block 参数为 false
selected, *received = chanrecv(c, elem, false)
return
}
即当没有值拿的时候,channel不做阻塞处理。直接返回false;
所以
select {
case v, ok = <-c:
// ... foo
default:
// ... bar
}
编译过来其实就是
for{
if selectnbrecv2(&v, &ok, c) {
// ... foo
} else {
// ... bar
}
}
以上,就是围绕channel阻塞的一些机制的分享,同样对于chansend相关的,其机制也是如此。
结尾这些其实相对而言是些细节上的东西,实际工作中可能用不上太多,但是当了解了后,会有恍然大悟整体通透的感觉,讲的比较粗糙,有什么问题大家该指出来就指出来。感谢
参考[1]:https://zhuanlan.zhihu.com/p/297053654 [golang channel 最详细的源码剖析]