深入学习Go-7 Channel
Go语言实现了CSP的并发编程模式,即不要通过共享内存来通信,而要通过通信来实现内存共享。channel就是各groutine之间通信的管道。
Channel读写逻辑
channel是一个结构体类型,其中包括数据缓冲区、等待读取的队列recvq和等待写入的队列sendq。
写入数据
当向channel中写入数据时,一般有三种情况:
1,写入数据时,当recvq队列不为空,说明缓冲区为空或着没有缓冲区,直接从recvq队列中取出等待读取的goroutine,写入数据并唤醒,写入过程结束。
2,如果recvq队列为空且缓冲区有空余空间,直接写入数据到缓冲区,写入过程结束。
3,如果recvq队列为空且缓冲区没有空余空间,数据写入到当前的goroutine并放入到sendq队列,进入休眠状态,等待被读取的goroutine唤醒。
读取数据
1,读取数据时,当sendq队列不为空且没有缓冲区,直接从sendq中取出等待写入的goroutine,从中读取数据并唤醒,结束读取过程。
2,如果sendq队列不为空且缓冲区已满,从缓冲区的头部读取数据。从sendq中取出等待写入的goroutine,将待读取的数据写入缓冲区尾部并唤醒,结束读取过程。
3,如果sendq队列为空且缓冲区中有数据,从缓冲区的头部读取数据,结束读取过程。
4,如果sendq队列为空且缓冲区中没有数据或没有缓冲区,将当前的goroutine放入到recvq队列,进入休眠状态,等待被写入的goroutine唤醒。
Channel读写特性
1,向一个nil channel发送数据,或者从一个nil channel读取数据,当前goroutine会阻塞。
2,向一个已经关闭的channel发送数据,会引起panic。
3,从一个已经关闭的channel读取数据,如果缓冲区为空,则返回类型的零值。
4,关闭一个已经关闭的channel,会引起panic。
优雅关闭Channel
当一个channel关闭时,其他goroutine向这个channel发送数据或再次关闭时,都会发生panic。
如何优雅的关闭channel,一个原则就是不要在receiver处关闭channel,也不要在多个sender处关闭channel。
我们重点看多个sender的情况:
多个 sender,一个 receiver
对于有多个sender,一个reciver的情况,需要增加一个额外用于传递close的channel。reciever调用close关闭channel,多个sender同时收到close信号,停止发送数据。例子如下:
func main() {rand.Seed(time.Now().UnixNano())dataChan := make(chan int, 10)stopChan := make(chan chan struct{})for i := 0; i < 5; i++ {go func(i int) {for {select {case <-stopChan:fmt.Printf("协程: %d 收到停止发送数据的信号\n", i)returncase dataChan <- rand.Intn(10):}}}(i)}go func() {for data := range dataChan {if data == 9 {fmt.Println("发送停止信号 ... ")close(stopChan)return}fmt.Println(data)}}()ch := make(chan os.Signal, 1)signal.Notify(ch, os.Interrupt)<-ch
}
多个 sender,多个 receiver
对于有多个sender,多个receiver的情况,除了需要增加一个用于传递close的channel,还需要一个再增加一个信号channel。
创建一个goroutine监听这个中间channel,sender或receiver发送关闭信号给这个中间channel,当收到关闭信号后关闭传递close的channel,这时多个sender或多个receiver收到close后,停止发送数据和停止接收数据。例子如下:
func main() {rand.Seed(time.Now().UnixNano())dataChan := make(chan int, 10)stopChan := make(chan struct{})toStop := make(chan string, 10)go func() {signal := <-toStopfmt.Println("停止信号: " + signal)close(stopChan)}()for i := 0; i < 5; i++ {go func(i int) {for {data := rand.Intn(10)if data == 9 {toStop <- "close signal by sender#" + strconv.Itoa(i)return}select {case <-stopChan:fmt.Printf("发送数据的协程: %d 收到停止发送数据的信号\n", i)returncase dataChan <- rand.Intn(10):}}}(i)}for i := 0; i < 5; i++ {go func(i int) {for {select {case <-stopChan:fmt.Printf("接收数据的协程: %d 收到停止发送数据的信号\n", i)returncase data := <-dataChan:if data == 9 {toStop <- "close signal by receiver#" + strconv.Itoa(i)return}}}}(i)}ch := make(chan os.Signal, 1)signal.Notify(ch, os.Interrupt)<-ch
}
总结
在这里我们主要讲了Channel的读写逻辑及读写特性;优雅关闭Channel的原则就是不要在receiver处关闭channel,也不要在多个sender处关闭channel。
更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号