深入学习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)
                    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。


更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号

图片

猜你喜欢

转载自blog.csdn.net/lonewolf79218/article/details/121944599