2 minutes
Golang Channel
channel的源代码: chan.go
关键的数据结构
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
lock mutex
}
type waitq struct {
first *sudog
last *sudog
}
sudog是等待的goroutine的表示, 参考 runtime2.go
type sudog struct {
g *g
isSelect bool
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
acquiretime int64
releasetime int64
ticket uint32
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
通过数据结构, 可以发现, hchan 使用环形队列组织数据, 发送和接受都是用 双向队列维护.
创建
channel的创建, 会调用 #makechan
- 如果是无界channel, 只分配基本的内存空间, 用首地址实现同步
- 如果是基本类型的channel, 直接分配完整的内存空间, 数据buf指向 内存空间首地址+hchanSize
- 指针类型的channel, 直接new方法分配
发送
当我们执行 c <- x 的语句的时候, 发生了什么?
chansend1(汇编入口 ) -> chansend -> sendDirect + goready(唤醒)
- 当channel是nil, 并且是阻塞调用, 挂起当前goroutine
- 当channel是nil, 并且是非阻塞调用, 直接返回
- 发送的时候会进行锁操作, 给已经关闭的channel 发送数据会抛出异常(不符合认知)
- 如果有 recvq有等待的goroutine, 直接发送给它, 并进行唤醒
- databuf还没有满的情况下, 将数据放入环形缓冲区,
- 放不下的情况, 放入senq的等待队列,
接收
当我们执行 x <- c, 执行流程
chanrecv1(汇编入口) -> chanrecv -> recv -> recvDirect + goready(唤醒)
快速检测: 针对channel没有关闭, 并且是非阻塞调用的情况下.
- unbuffer channel没有发送的队列(sendq)
- buffer channel没有数据发送(qcount=0) 流程:
- 当channel是nil, 并且是阻塞调用, 会挂起
- 当channel是nil, 并且是非阻塞调用, 直接返回
- senq有阻塞的sender, 直接接收, 同时唤醒sender goroutine
- 有数据的情况下, 复制数据,
- 非阻塞调用, 直接返回
- 构建sudog, 放入recvq.
close
方法入口 #closechan
- 关闭多次, 会panic
- 遍历recvq中阻塞的等待, 进行释放
- 遍历sendq中阻塞的等待, 进行释放
- 遍历之前需要释放的goroutine, 进行唤醒
select 模型:
select的send:
selectnbsend(编译入口) -> chansend, 不阻塞的调用
selectnbrecv(编译入口) -> chanrecv, 不阻塞的调用
ok类型的receive:
selectnbrecv2(编译入口) -> chanrecv, 不阻塞的调用
相比之前的receive操作, 多了对nil的判断, 意义不是特别大.
faq: 那么, 在使用上, 向一个nil的 channel 进行 receive 和 send会怎么样? 原理分析: 1. receive 情况: 1. select模型下, 都是使用 非阻塞的调用, 会返回失败 2. 常规使用, 没有ok的情况下, 都是阻塞调用, block forever(gopark), 可能没有线程执行, 会抛出异常 3. 常规使用, 有ok的情况下, 不阻塞, ok是false 2. send 情况: 1. select模型下, 使用非阻塞调用, 会返回失败 2. 常规使用, 都是阻塞调用, block forever (gopark), 可能没有线程执行, 会抛出异常 因为不是golang风格的异常, 所以, 使用defer也不会被检查出来
实验:
-
常规空指针接收:
-
常规空指针发送:
-
空指针 + select send
-
空指针 + select receive
补充说明:
上面的4个实验, 看上去都有报错, 其实是因为 当前的main函数的goroutine被阻塞, 导致没有可执行的goroutine导致的报错, 添加一个可运行的goroutine就可以了, 实验如下图:
参考:
321 Words
2019-04-21 11:23 +0800