channel详解
CSP模型
go语言的并发是基于CSP并发模型实现的。
golang的并发哲学:Do not communicate by sharing memory; instead, share memory by communicating。
channel主要用于在多个goroutine之间进行通信。
channel是并发安全的。
channel的使用方法:
var ch chan int // 声明一个int类型的channel,不进行初始化
var ch = make(chan int) // 创建一个无缓冲的channel
var ch = make(chan int,1) // 创建一个有缓冲的channel
var ch = make(<-chan int,1) // 创建一个只读的单向channel
var ch = make(chan<- int,1) // 创建一个只写的单向channel
ch <- 1 // 向channel中发送一个值
x,ok := <- ch // 从channel中接收一个值,如果channel关闭,则ok返回false,通过判断ok的值可以判断channel是否已经关闭
close(ch) // 关闭channel
for item := range ch {} // 从channel中读取值,如果channel关闭,读取完值后会退出循环,否则会阻塞
channel使用原则:
- 无缓冲的channel是同步的,有缓冲的channel是异步的
- 对未初始化的channel进行关闭操作,会引发panic
- 对未初始化的channel进行发送或接收数据,会造成阻塞
- 向无缓冲的channel发送或接收数据会发送阻塞
- 向有缓冲的channel发送数据,如果channel容量已满,会发生阻塞
- 从有缓冲的channel接收数据,如果channel已经为空且channel未关闭,会发送阻塞
- 从有缓冲的channel接收数据,如果channel已经为空且channel已经关闭,会读取到数据类型的零值
- 向已经关闭的channel发送数据,会引发panic
- 关闭一个已经关闭的channel会导致panic
channel的底层实现
channel相关实现的源码位于:src/runtime/chan.go
- hchan
type hchan struct {
qcount uint // 当前队列的长度,len()的结果
dataqsiz uint // 循环队列大小,cap()的结果
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16 // 元素大小
closed uint32 // 是否关闭
elemtype *_type // 元素类型
sendx uint // 环形队列下一次发送元素的索引
recvx uint // 换循队列下一次接收元素的索引
recvq waitq // 接收的goroutine等待队列
sendq waitq // 发送的goroutine等待队列
// 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 // 互斥锁,保证同步安全
}
type waitq struct {
first *sudog
last *sudog
}
hchan为一个环形队列,waitq链表表示被阻塞的goroutine的等待队列。
-
makechan 为环形队列申请内存
-
chansend
- 获取同步锁,对缓冲队列进行加锁
- 如果有等待的goroutinue,那么数据直接发送给这个接收goroutine,数据拷贝1次
- 如果缓冲队列没有满,将发送的数据拷贝到队列中,sendx加1
- 如果缓冲队列满了,那么将发送goroutine放到发送阻塞队列中
-
chanrecv
- 对于队列进行加锁
- 如果有send阻塞队列,那么
- 如果channel没有缓冲区,那么直接从send goroutine接收数据
- 否则从缓冲区队列头取出数据,并且唤醒send开始写入数据到队列尾部
- 缓存队列不为空,那么从队列获取数据,数据都是拷贝出来的。
- 缓存队列为空,那么将recv goroutine加入到recv阻塞队列中
-
closechan
-
将ch.closed设置为1
-
唤醒 recvq 队列里面的阻塞 goroutine
-
唤醒 sendq 队列里面的阻塞 goroutine
处理方式是分别遍历 recvq 和 sendq 队列,将所有的 goroutine 放到 glist 队列中,最后唤醒 glist 队列中的 goroutine.
-
select
select的调度方法为selectgo,源码位于src\runtime\select.go 文件
最终会调用chan.go中的selectrecv、selectsend等方法。
- Go的CSP并发模型实现:M, P, G
- Golang-Channel原理解析
- https://github.com/studygolang/go-collection/blob/main/go-article/chan.md