go channel 基础(如何优雅地关闭 channel)

前言

想必听说过 go 的,应该都知道 go 的最大的特性 goroutine 并发编程,而说到并发编程,使用 channel 进行数据传输是 go 中的必修课。

go 的并发哲学:不要通过共享内存来通信,而要通过通信来实现内存共享。

channel 的坑不少,本篇简单聊聊关闭 channel 的方法。

关闭 channel 的基本原则

坊间流传的关闭 channel 的原则:

不要从接收端关闭 channel,也不要在有多个发送端时,主动关闭 channel

这个原则的来源就因为:

  1. 不能向已关闭的 channel 发送数据
  2. 不能重复关闭已关闭的 channel

如何关闭

  1. 比较粗暴的方式,使用 defer-recovery 机制,在关闭的时候如果 panic 了,也会被 recovery
  2. 既然 channel 只能 close 一次,那么 go 的源码包中的 sync.Once 就可以派上用场了,专门做这种事情的。

接下来根据 senderreceiver 的个数,分如下几种情况:

  1. senderreceiver
  2. senderreceiver
  3. senderreceiver
  4. senderreceiver

第 1,2 种情况,直接在 sender 端关闭 channel 即可。

func main() {
    
    
    dataCh := make(chan int, 100)
    
	// sender
    go func() {
    
    
        for i := 0; i < 1000; i++ {
    
    
            dataCh <- i + 1
        }
        log.Println("send complete")
        close(dataCh)
    }()
    
	// receiver
    for i := 0; i < 5; i++ {
    
    
        go func() {
    
    
            for {
    
    
                data, ok := <-dataCh
                if !ok {
    
     // 已关闭
                    return
                }
                _ = data
            }
        }()
    }
    
    select {
    
    
	case <-time.After(time.Second * 5):
		fmt.Println(runtime.NumGoroutine())
    }
}

第 3 种情况,可以增加一个传递关闭信号的 stopCh,在 receiver 端通过 stopCh 下达关闭数据 dataCh 的指令。sender 监听到关闭信号后,不再向数据 dataCh 发送数据。

package main

import (
	"time"
	"math/rand"
	"sync"
	"log"
)

func main() {
    
    
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)

	const Max = 100000
	const NumSenders = 1000

	wgReceivers := sync.WaitGroup{
    
    }
	wgReceivers.Add(1)

	dataCh := make(chan int)
	stopCh := make(chan struct{
    
    })

	// senders
	for i := 0; i < NumSenders; i++ {
    
    
		go func() {
    
    
			for {
    
    
				select {
    
    
				case <- stopCh:
					return
				default:
				}

				select {
    
    
				case <- stopCh:
					return
				case dataCh <- rand.Intn(Max):
				}
			}
		}()
	}

	// receiver
	go func() {
    
    
		defer wgReceivers.Done()

		for value := range dataCh {
    
    
			if value == Max-1 {
    
    
				close(stopCh)
				return
			}

			log.Println(value)
		}
	}()

	wgReceivers.Wait()
}

第 4 种情更为复杂一点,不能够像第 3 种情况那样直接在 receiver 端关闭 stopCh,这样会导致重复关闭已关闭的 channelpanic。因此需要再加个中间人 toStop 来接收关闭 stopCh 的请求。

package main

import (
	"time"
	"math/rand"
	"sync"
	"log"
	"strconv"
)

func main() {
    
    
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)

	const Max = 100000
	const NumReceivers = 10
	const NumSenders = 1000

	wgReceivers := sync.WaitGroup{
    
    }
	wgReceivers.Add(NumReceivers)

	dataCh := make(chan int)
	stopCh := make(chan struct{
    
    })
	
	// 这个是添加的中间人,通过它来接收关闭 stopCh 的请求,做一次关闭
	// 这里给缓存是 goroutine 启动时机,可能导致 select 选择,导致逻辑问题
	toStop := make(chan string, 1)

	var stoppedBy string
	go func() {
    
    
		stoppedBy = <-toStop
		close(stopCh)
	}()

	// senders
	for i := 0; i < NumSenders; i++ {
    
    
		go func(id string) {
    
    
			for {
    
    
				value := rand.Intn(Max)
				if value == 0 {
    
    
					select {
    
    
					case toStop <- "sender#" + id:
					default:
					}
					return
				}

				// 由于 select 是随机选择的,所以先在这里尝试得知是否关闭
				select {
    
    
				case <- stopCh:
					return
				default:
				}

				select {
    
    
				case <- stopCh:
					return
				case dataCh <- value:
				}
			}
		}(strconv.Itoa(i))
	}

	// receivers
	for i := 0; i < NumReceivers; i++ {
    
    
		go func(id string) {
    
    
			defer wgReceivers.Done()

			for {
    
    
				select {
    
    
				case <- stopCh:
					return
				default:
				}

				
				select {
    
    
				case <- stopCh:
					return
				case value := <-dataCh:
					if value == Max-1 {
    
    
						select {
    
    
						case toStop <- "receiver#" + id:
						default:
						}
						return
					}

					log.Println(value)
				}
			}
		}(strconv.Itoa(i))
	}

	wgReceivers.Wait()
	log.Println("stopped by", stoppedBy)
}

这个例子可以在 senderreceiver 端都发送关闭信号,通过 toStop 这个中间人来传递关闭信号,接收到之后关闭 stopCh。这里需要注意将 toStop 定义为带缓冲的 channel,若是不带缓冲,可能会出现 <-toStop 这个接收协程还未跑起来时,就已经有其他协程向其发送了 toStop<-xx 关闭信号。
这时在 senderreceiverselect 分支就可能走 default 语句,导致逻辑错误。

这个例子中,简单点的做法可以给 toStop 设置缓存为 senderreceiver 的和,就可以简写为如下:

...
toStop := make(chan string, NumReceivers + NumSenders)
...
        value := rand.Intn(Max)
        if value == 0 {
    
    
            toStop <- "sender#" + id
            return
        }
...
        if value == Max-1 {
    
    
            toStop <- "receiver#" + id
            return
        }
...

channel 的注意点

  • channel 的声明必须使用 make 关键字,不能直接 var c chan int,这样得到的是 nil channel
  • 不能向 nil channel 发送数据
var c chan int
c <- 1 // panic
  • 已关闭的 channel 不能再往其发送数据
c := make(chan int)
close(c)
c <- 1 // panic
  • 不能重复关闭已关闭的 channel
c := make(chan int)
close(c)
close(c) // panic
  • 只要你的 channel 没有引用关系了,就算你没有 close 关闭或者 channel 有大量的堆积数据没有消费,最终都会被 gc 释放。

总结

关闭 channel 的基本法则:

  • sender 的情况下,都可以直接在 sender 端关闭 channel
  • sender 的情况下,可以增加一个传递关闭信号的 channel 专门用于关闭数据传输的 channel

原则:不要从接收端关闭 channel,也不要在有多个发送端时,主动关闭 channel

本质:已关闭的 channel 不能再关闭(或者再向其发送数据)

channel 的使用非常多样,本篇列举了几个基本的场景,真正想要融会贯通,还是得要多写多思考。

参考

猜你喜欢

转载自blog.csdn.net/DisMisPres/article/details/128655102