如何优雅的关闭Go Channel

Channel关闭原则

不要再消费端关闭channel,不要在有多个并行的生产者时对Channel执行关闭操作

也就是说应该只在[唯一的或者最后唯一剩下的]生产者协程中关闭channel,来通知消费者已经没有值可以继续读了。

暴力的关闭channel正确方法

如果想要在消费端关闭channel,或者在多个生产者端关闭channel,可以使用recover机制来上个保险,避免程序因为panic崩溃。

消费端关闭

package main

import "fmt"
func main() {
	//var i chan int
	c :=make( chan int,5)
	c <-1
	SafeClose(c)
}

func SafeClose(ch chan int) (justClosed bool) {

	defer func() {
		if recover() != nil {
			justClosed = false
		}
	}()

	// assume ch != nil here.
	ii:= <-ch
	fmt.Println(ii)
	close(ch)
	return true
}

使用这种方法明显违背了上面的channel关闭原则,然后性能还可以,毕竟在每个协程只会调用一次SafeClose,性能损失很小。

生产端关闭
package main

import "fmt"
func main() {
	//var i chan int
	c :=make( chan int,5)
	safesend(c,10)
	SafeClose(c)
}

func safesend(ch chan int ,v int) (closed bool)  {

	defer func() {
		if recover() !=nil {
			closed =true
			fmt.Println("true")
		}
	}()
	ch <- v
	return false
}

func SafeClose(ch chan int) (justClosed bool) {

	defer func() {
		if recover() != nil {
			justClosed = false
			fmt.Println("false")
		}
	}()

	// assume ch != nil here.
	ii:= <-ch
	fmt.Println(ii)
	close(ch)
	return true
}
礼貌的关闭channel方法

使用sync.Once来关闭channel,确保只会关闭一次

拓展:
sync.Once 是Golang package 中使方法只执行一次的对象实现,作用和init函数相同。但也用所不同

  • init函数是在文件包首次被加载的时候执行,切只执行一次
  • sync.Once是在代码运行中需要的时候执行,且只执行一次

sync.Once拓展连接


package main

import (
	"sync"
)
func main() {
	NewMyChannel()
}

type MyChannel struct {
	C    chan int
	once sync.Once
}

func NewMyChannel() *MyChannel {
	return &MyChannel{C: make(chan int)}
}

func (mc *MyChannel) SafeClose() {
	mc.once.Do(func() {
		close(mc.C)
	})
}

使用sync.Mutex达到同样的目的。

package main

import (
	"sync"
)
func main() {
	NewMyChannel()
}


type Myhannel struct {
	c chan int
	closed bool
	mutex sync.Mutex
}

func NewMyChannel() *Myhannel  {
	return &Myhannel{c: make(chan int)}
}
func (mc *Myhannel) safeClose()  {
	mc.mutex.Lock()
	if !mc.closed {
		close(mc.c)
		mc.closed=true
	}
	mc.mutex.Unlock()
}

func (mc *Myhannel) IsClosed() bool  {
	mc.mutex.Lock()
	defer mc.mutex.Unlock()
	return mc.closed
}

要知道golang的设计者不提供SafeClose或者SafeSend方法是有原因的,他们本来就不推荐在消费端或者在并发的多个生产端关闭channel,比如关闭只读channel在语法上就彻底被禁止使用了。

优雅的关闭channel的方法

多个消费者,单个生产者。这种情况最简单,直接让生产者关闭channel好了。

package main

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

func main() {

	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)

	const MaxRandomNumber = 100000
	const NumReceivers  =100

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

	dataCh :=make(chan int,100)

	go func() {
		for {
			if value :=rand.Intn(MaxRandomNumber);value ==0 {
				//只有发送方可以安全地关闭通道。
				close(dataCh)
			}else {
				dataCh <- value
			}
		}
	}()

	for i :=0;i<NumReceivers;i++ {
		go func() {
			defer wgReceivers.Done()
			//接收值,直到dataCh关闭
			//dataCh的值缓冲区队列为空
			for value :=range dataCh{
				log.Println(value)
			}
		}()

	}
	wgReceivers.Wait()
}

多个生产者,单个消费者。这种情况要比上面的复杂一点。我们不能在消费端关闭channel,因为这违背了channel关闭原则。但是我们可以让消费端关闭一个附加的信号来通知发送端停止生产数据。

package main

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

func main() {

	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)

	const MaxRandomNumber = 100000
	const NumReceivers  =100

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

	dataCh :=make(chan int,100)
//stopCh是一个附加的信号通道。它的发送方是通道数据的接收方。它的接收者是信道数据的发送者。
	stopCh:=make(chan struct{})

	for i :=0;i<NumReceivers;i++ {
		go func() {
			for {
			//这里的第一个选择是尝试尽早退出goroutine。事实上,对于这个例子来说,它不是必要的,所以可以省略。
				select {
				case <-stopCh:
					return
				default:

				}
				// Even if stopCh is closed, the first branch in the
				// second select may be still not selected for some
				// loops if the send to dataCh is also unblocked.
				// But this is acceptable, so the first select
				// can be omitted.
				select {
				case <-stopCh:
					return
				case dataCh <- rand.Intn(MaxRandomNumber):

				}
			}
		}()
	}

	go func() {
		defer wgReceivers.Done()

		for value :=range dataCh{
			if value == MaxRandomNumber -1 {
			//dataCh通道的接收方也是stopCh cahnnel的发送方。在这里关闭停止通道是安全的。
				close(stopCh)
				return
			}
			log.Println("输出",value)
		}
	}()
	wgReceivers.Wait()
}

多个生产者,多个消费者

package main

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

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

	// ...
	const MaxRandomNumber = 100000
	const NumReceivers = 10
	const NumSenders = 1000

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

	// ...
	dataCh := make(chan int, 100)
	stopCh := make(chan struct{})
	// stopCh is an additional signal channel.
	// Its sender is the moderator goroutine shown below.
	// Its reveivers are all senders and receivers of dataCh.
	toStop := make(chan string, 1)
	// The channel toStop is used to notify the moderator
	// to close the additional signal channel (stopCh).
	// Its senders are any senders and receivers of dataCh.
	// Its reveiver is the moderator goroutine shown below.

	var stoppedBy string

	// moderator
	go func() {
		stoppedBy = <- toStop
		close(stopCh)
	}()

	// senders
	for i := 0; i < NumSenders; i++ {
		go func(id string) {
			for {
				value := rand.Intn(MaxRandomNumber)
				if value == 0 {
					// Here, a trick is used to notify the moderator
					// to close the additional signal channel.
					select {
					case toStop <- "sender#" + id:
					default:
					}
					return
				}

				// The first select here is to try to exit the goroutine
				// as early as possible. This select blocks with one
				// receive operation case and one default branches will
				// be optimized as a try-receive operation by the
				// official Go compiler.
				select {
				case <- stopCh:
					return
				default:
				}

				// Even if stopCh is closed, the first branch in the
				// second select may be still not selected for some
				// loops (and for ever in theory) if the send to
				// dataCh is also unblocked.
				// This is why the first select block is needed.
				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 {
				// Same as the sender goroutine, the first select here
				// is to try to exit the goroutine as early as possible.
				select {
				case <- stopCh:
					return
				default:
				}

				// Even if stopCh is closed, the first branch in the
				// second select may be still not selected for some
				// loops (and for ever in theory) if the receive from
				// dataCh is also unblocked.
				// This is why the first select block is needed.
				select {
				case <- stopCh:
					return
				case value := <-dataCh:
					if value == MaxRandomNumber-1 {
						// The same trick is used to notify
						// the moderator to close the
						// additional signal channel.
						select {
						case toStop <- "receiver#" + id:
						default:
						}
						return
					}

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

	// ...
	wgReceivers.Wait()
	log.Println("stopped by", stoppedBy)
}
最本质的原则就只有一条:

不要关闭(或将值发送到)已关闭的通道

你可能感兴趣的:(Go基础)