go-channel
通过一掉题目,抛砖引玉,了解一下channel
和select
的作用:
1 | func main() { |
结果输出为321
channel收发数据的机制
channel收发数据的机制如下表所示:
channel为以下四种状态时的操作 | 无缓冲区 | 已关闭 | nil | 空的 | 非空非满 | 满了 |
---|---|---|---|---|---|---|
往channel发数据 | 阻塞 | panic | 阻塞 | 发送成功 | 发送成功 | 阻塞 |
从channel读数据 | 阻塞 | 先读完原有数据,再读到存储的元素类型的零值 | 阻塞 | 阻塞 | 接收成功 | 接收成功 |
关闭channel | 关闭成功 | painc | panic | 关闭成功 | 关闭成功 | 关闭成功 |
- 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据
select
的运行机制
- 选取一个可执行不阻塞的
case
分支,如果多个case
分支都不阻塞,会随机算一个case
分支执行,和case
分支在代码里写的顺序没关系。 - 如果所有
case
分支都阻塞,会进入default
分支执行。 - 如果没有
default
分支,那select
会阻塞,直到有一个case
分支不阻塞。
channel底层
channel
源码如下:
1 | func makechan(t *chantype, size int) *hchan |
通过make
函数来创建channel
时,Go会调用运行时的makechan
函数。
从上面的代码可以看出makechan
返回的是指向channel
的指针。
因此channel
作为函数参数时,实参channel
和形参channel
都指向同一个channel
结构体的内存空间,所以在函数内部对channel
形参的修改对外部channel
实参是可见的,反之亦然。
向 channel 写数据的流程: 如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程; 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程; 如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒;
向 channel 读数据的流程: 如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程; 如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程; 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒;
读写流程总结:先从缓冲区buf中读写数据,再考虑是否从recvq或者sendq队列读写数据
使用场景: 消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步