Channel知识点目录
简单梳理一下Go中Channel的知识点
一、Channel
Channel介绍:
Channel源于CSP模型,在Go的并发中,提倡使用通信来共享内存而不要使用共享内存来通信。Go内的Channel实现了Goroutine之间的数据的读写操作,在任何时刻,只能有一个Goroutine能对Channel
中的数据进行操作,Channel
可以看作一个FIFO的消息队列。
Channel的特点:
- 只能传输一种数据类型
chan
是引用类型,底层是一个指针。make之后才能使用,如果不make,得到的是nil的chan。初始容量确定后不会再改变- 线程安全,无需加锁,Channel结构体会在执行接收操作和发送操作时内部上锁
- 阻塞操作,缓冲区满时无法再发送数据到chan;缓冲区空时无法再从chan中获取数据
Channel的创建:
ch := make(chan int, 3) // 创建缓冲区大小为3的chan
ch := make(chan int) // 无缓冲区的chan
var ch chan int // 声明一个chan,但为nill,如果往nill的chan中发送/接收数据会引发deadlock
Channel必须make初始化后才能使用
Channel结构体:hchan
type hchan struct {
qcount uint // 循环队列中元素的个数
dataqsiz uint // 循环队列的大小
buf unsafe.Pointer // 循环队列中的数据指针
elemsize uint16 // channel中元素的大小
closed uint32 // channel是否关闭
elemtype *_type // channel中的元素类型
sendx uint // 往chan中发送数据chan<-,在buf中的位置
recvx uint // 从chan中取出数据<-chan,在buf中的位置
recvq waitq // g想执行<-chan操作接收数据时,如果chan无数据,会被放入recvq中
sendq waitq // g想执行chan<-操作发送数据时,如果chan已经满,会被仿佛sendq中
lock mutex // 互斥锁,用于保护所有字段
}
二、sendq 和 recvq 的作用
我们通过ch := make(chan int, 3)
创建一个缓冲区大小为3的chan
。我们有一个Goroutine,简称为G1
sendq
-
在G1中执行三次
ch <-1
的操作后,chan
缓冲区已满 -
然后G1还想执行一次
ch<-value
的操作时,因为缓冲区已满,无法再发送数据进chan
中,因此G1执行gopark
主动调用Go的调度器scheduler,让出与G1绑定的M -
G1会抽象成一个包含指向G1的指针以及包含value元素的
sudog
结构体,然后存放在sendq中等待被唤醒唤醒时机:当另一个Goroutine,假设为G2,想从
chan
中获得数据时,G2执行<-ch
操作,从缓存队列中取出数据
唤醒方式:channel会将G1的sudog
结构体取出并且将value数据放进缓存队列中,然后唤醒G1,放回可执行队列中
recvq
- 在G1中直接从刚初始化的chan中执行接收数据的操作
<-ch
- 此时chan中没有任何数据,那么G1会阻塞,G1调用
gopark
调用scheduler,让出与G1绑定的M,使M可以和其他G绑定 - G1会抽象成一个包含指向G1的指针以及空元素的sudog结构体,然后存放在recvq中等待被唤醒
唤醒时机:当有另外一个Goroutine,假设为G2,想往chan
中发送数据时,G2执行ch<-
操作,往缓存队列中发送数据
唤醒方式(与sendq中的唤醒方式不同!!):G2并没有锁住chan
把数据放在缓存队列中,而是直接把数据copy到了G1中,减少了内存的拷贝以及加锁释放锁的时间花费。最后唤醒G1,放回可执行队列中
三、不同情况下Channel的<-ch
和ch<-
操作
无缓冲Chan
<-ch
:阻塞
ch<-
:阻塞
有缓冲Chan
<-ch
:缓冲区中存在数据,不阻塞;缓冲区无数据,阻塞
ch<-
:缓冲区中数据未满,不阻塞;缓冲区中数据已满,阻塞
已关闭的Chan
<-ch
:如果Chan中还有数据没有读取,则返回数据和数据是否有效的bool;如果没有数据,则返回0值和false
ch<-
:panic
重复关闭已关闭的Chan
:panic
nil Chan
<-ch
:死锁
ch<-
:死锁
关闭nill Channel
:panic