前言

本文主要围绕channel的一些细节上做些总结和分享。主要涉及到两个方面一个是初始化,另外一个就是在数据实际接收和发送的时候的一些细节;这两个离不开golang编译器的协助,所以在了解go源码时,go编译器其实在其中起到了很大的作用。

makechan

make 方法 主要是初始化 切片、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编译器根据不同的使用场景编译成不同的方法chanrecv1chanrecv2来用的。
比如

//单纯的 :
 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 最详细的源码剖析]