go Channel源码分析
理论基础
不要通过共享内存来通信,要通过通信来共享内存。
特点
通道分为非缓冲通道和缓冲通道。
单向通道由双向通道转换而来,但是单向通道不能转换回双向通道。
通过make来初始化一个chan,未初始化的chan的零值是nil。
接收数据时,返回两个值。第一个值是返回的chan中的元素,第二个值是bool类型,代表是否成功从chan中读取到一个值。如果是false,那么chan已经被close而且chan中没有元素,第一个返回值是零值。
不要在双向通道的接收端关闭通道,由写入者关闭通道,避免向已经关闭的通道写数据导致的panic。
panic
(1)向已经关闭的通道写数据
(2)重复关闭通道
(3)close nil的channel
阻塞
(1)向未初始化(nil channel)的通道写数据或读数据
(2)向缓冲区已满的通道写入数据
(3)通道中没有数据,读取该通道
使用
定义
var c chan int // c == nil,c is channel of int
c := make(chan int)
c := make(chan<- int) // 定义只能发送数据的通道
c := make(<-chan int) // 定义只能接收数据的通道
非缓冲通道的发送与接收默认是阻塞的
当数据写入通道时,在发送数据的语句处阻塞,直到其他协程从通道读取数据。
当从通道读取数据时,在读取数据的语句处阻塞,直到其他协程把数据写入通道。
package main
import (
"fmt"
)
func hello(done chan bool) {
fmt.Println("Hello world goroutine")
done <- true
}
func main() {
done := make(chan bool)
go hello(done)
<-done
fmt.Println("main function")
}
运行结果
死锁(所有协程都阻塞)
package main
func main() {
ch := make(chan int)
ch <- 5
<-ch
}
非缓冲通道造成goroutine泄漏
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan bool)
go func() {
time.Sleep(2 * time.Second)
ch <- true // block
fmt.Println("end")
}()
select {
case <-ch:
fmt.Println("get data")
case <-time.After(time.Second):
fmt.Println("timeout")
}
}
运行结果
通道中持续接收元素(直到通道关闭且通道里面没有元素)
for c := range ch {
fmt.Println(c)
}
实现原理
go/src/runtime/chan.go
chan是多生产者多消费者的模式,通过循环队列+mutex来存取元素。如果消费者因为没有数据可取而被阻塞了,就会被加入到recvq队列中。如果生产者因为循环队列满了而阻塞,会被加入到sendq队列中。
send
如果chan是nil,那么永久阻塞调用者。
如果chan已经被close,那么会panic。
针对非缓冲通道场景,如果recvq中有receiver,那么唤醒一个receiver,把数据交给它。
如果循环队列未满,那么把数据放入到循环队列中。
如果循环队列满了,那么把sender加入sendq,sender阻塞等待,直到循环队列未满或者chan被close。
recv
如果chan是nil,那么永久阻塞调用者。
如果chan已经被close,并且循环队列中没有元素,那么返回零值和false。
针对非缓冲通道场景,如果sendq中有sender,那么唤醒一个sender,把sender的数据交给receiver。
如果循环队列满了,那么取出首元素给receiver。这时,如果sendq中有sender,那么唤醒一个sender并把sender的数据放入循环队列。
如果循环队列中有元素,那么取出首元素给receiver。
如果循环队列中没有元素,那么把receiver加入recvq,receiver阻塞等待,直到循环队列有元素或者chan被close。
close
如果chan是nil或者已经被close,那么会panic。
唤醒sendq中的sender和recvq中的receiver。