go channel basics (how to close the channel gracefully)

Preface

Those who have heard of it gomust know gothat its biggest feature goroutineis concurrent programming, and when it comes to concurrent programming, using it channelfor data transmission is goa required course in it.

goThe concurrency philosophy: Don't communicate through shared memory, but implement memory sharing through communication.

channelThere are many pitfalls. This article will briefly talk about channelthe method of closing them.

Basic principles for closing channels

The widely circulated channelprinciple of closure:

Do not close from the receiving end channel, and do not actively close when there are multiple senders.channel

The origin of this principle is because:

  1. channelCannot send data to closed
  2. Cannot re-close an already closedchannel

How to close

  1. The more crude method is to use defer-recoverythe mechanism. If it is blocked when closing panic, it will also be blocked recovery.
  2. Since channelcan only be used closeonce, then gothe in the source code package sync.Oncecan come in handy, specifically for doing this kind of thing.

Next, according to the number of senderand receiver, the following situations are divided:

  1. senderSingle Singlereceiver
  2. Single senderand multiplereceiver
  3. Long sendersinglereceiver
  4. senderManyreceiver

In cases 1 and 2, just senderclose it directly on the end 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())
    }
}

In the third case, you can add a shutdown signal stopChand issue an instruction to close the data at receiverthe end . After listening to the shutdown signal, no more data is sent to the data.stopChdataChsenderdataCh

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()
}

receiverThe fourth case is more complicated and cannot be closed directly on the end like the third case stopCh. This will result in repeated closing of the closed channelend panic. Therefore, a middleman needs to be added toStop to receive the shutdown stopChrequest.

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)
}

This example can send a shutdown signal to both the senderand receiverend, toStoppass the shutdown signal through this middleman, and close it after receiving it stopCh. It should be noted here that toStopit is defined as buffered channel. If it is not buffered, it may happen that <-toStopother coroutines have already sent toStop<-xxshutdown signals to it before the receiving coroutine starts running. At this time, the statement may be taken
in the senderor branch , resulting in logic errors.receiverselectdefault

In this example, a simpler approach can be to toStopset the cache to senderthe sum receiverof and , which can be abbreviated as follows:

...
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
        }
...

Notes on channels

  • channelThe declaration must use makethe keyword, not directly var c chan int, so what you get isnil channel
  • Unable to nil channelsend data to
var c chan int
c <- 1 // panic
  • Closed channelNo more data can be sent to it
c := make(chan int)
close(c)
c <- 1 // panic
  • Cannot re-close an already closedchannel
c := make(chan int)
close(c)
close(c) // panic
  • As long as you channelhave no reference relationship, even if you have not closeclosed it or channelthere is a large amount of accumulated data that has not been consumed, it will eventually be gcreleased.

Summarize

channelBasic rules for closing :

  • In single sendercase, it can senderbe closed directly on the client side channel.
  • In many sendercases, you can add a shutdown signal that channelis specifically used to shut down data transmission channel.

Principle: Do not close from the receiving end channel, and do not actively close when there are multiple senderschannel .

Essence: What has been closed channelcannot be closed again (or data can be sent to it again) .

channelThe use of is very diverse. This article lists several basic scenarios. If you really want to understand it, you still have to write more and think more.

reference

おすすめ

転載: blog.csdn.net/DisMisPres/article/details/128655102