提起go里边的管道无人不知无人不晓,go原理中推荐的就是通过管道来通信,而不是通过共享内存来通信。
我们先创建一个channel
channel的初始化有两种:
// 这种方式只声明不初始化
var ch chan int
// 使用make
ch1 := make(chan string) // 无缓冲的channel
ch2 := make(chan string,5) // 有缓冲的channel
此外还有channel 特有的操作符
ch := make(chan int,10)
ch <- 1 // 数据流入管道
for _,v := range ch {
fmt.Println(v)// 数据流出管道
}
一般使用内置函数close 关闭 channel
内置函数len cap 分别查询缓冲区中数据的个数以及缓冲区的大小
当管道是无缓冲区的管道的时候,从管道中读取和写入都会阻塞,知道有写成从管道写入或读取数据
管道有缓冲区但缓冲区无数据的时候读取也会阻塞,直到有协程写入数据。类似向管道写入数据如果缓冲区已满的话也是无法写入的
对于值为nil的管道,无论读写都会阻塞,永久阻塞
关闭已经关闭的管道或者是向已经关闭的管道中进行写入会触发Panic
管道读取的时候类似map 最多可以有两个返回值,第一个表示读出的数据,第二个表示是否成功读取了数据
管道事一种先入先出的队列 FIFO 数据总是按照写入的顺序流出管道
协程读取管道时,阻塞的条件有:
- 管道无缓冲区
- 管道缓冲区无数据
- 管道初始化为nil,但并未分配内存
协程写入管道,阻塞的条件有:
- 管道无缓冲区
- 管道缓冲区已满
- 管道初始化为nil,但并未分配内存
管道内部结构以及底层原理
channel 的源码在src/runtime/chan.go:chan
type hchan struct {
qcount uint // 当前队列中剩余的元素
dataqsiz uint // 环形队列的长度,即可以存放的元素个数
buf unsafe.Pointer // points to an array of dataqsiz elements 指向内存的指针
elemsize uint16 // 每个元素的大小
synctest bool // true if created in a synctest bubble
closed uint32 // 标识关闭
timer *timer // timer feeding this chan
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 protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex // 互斥锁 chan不允许并发读写
}
buf 指针指向的是一个环形队列做为缓冲区
使用数组来实现这个队列
sendx,recvx分别表示队尾和对首
dataqsiz指示队列长度为6,即可以缓冲6个元素
qcount 表示目前队列中还有两个元素
等待队列
从管道读取数据时,如果管道缓冲区为空或者没有缓冲区,当前协程会被阻塞,并加入recvq队列
向管道写入数据,如果缓冲区已满或者没有缓冲区,则当前协程会被阻塞并加入sendq队列
处于等待队列的协程会在其他协程操作管道时被唤醒:
- 因读阻塞的协程会被向管道写入数据的协程唤醒
- 因写阻塞的协程会被从管道读取数据的协程唤醒
一般情况下recvq snedq 一般至少一个为空,只有一个例外,那就是同一个协程使用select语句向管道一边写入数据。一边读取数据,此时协程会分别位于两个等待队列中
创建 写数据 读取数据 时内部流程
创建管道:
就是初始化hchan结构体
缓存区长度由内置函数make设置
buf的大小由元素大小和缓冲区长度共同决定
向管道中写入数据:
- 如果缓冲区有空余位置,则将数据写入缓冲区,结束发送过程
- 如果缓冲区没有空余位置,将协程加入sendq队列,等到读协程唤醒
-当recvq不为空时说明有一个协程在等待接收数据 很明显此时缓冲区为空,那么写协程进入后不用写入缓冲区,直接传递给recvq中的第一个
从管道读数据:
- 如果缓冲区有数据,直接读出
- 如果缓冲区没有数九,将协程加入recvq队列,进入睡眠并等待被写协程唤醒
同上在sendq不为空的情况下,且没有缓冲区,直接获取数据,不用写入缓冲区
关闭管道时:
关闭管道时会把recvq中的协程全部唤醒,这些协程获取的数据都为对应类型的灵芝,sendq的协程也会唤醒,但是相当于向关闭的channel ,里边写数据有可能触发Panic
会触发panic的操作:
关闭值为nil的管道
关闭已经被关闭的管道
向已经关闭的管道写入数据