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)
return
case 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 := <-toStop
fmt.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)
return
case dataChan <- rand.Intn(10):
}
}
}(i)
}
for i := 0; i < 5; i++ {
go func(i int) {
for {
select {
case <-stopChan:
fmt.Printf("接收数据的协程: %d 收到停止发送数据的信号\n", i)
return
case 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。
更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号