Go study notes - how to shutdown gracefully Go Channel [translation]

Channel Closed Principle

Do not close the channel at the consumer side, not performing the closing operation of the channel when there are multiple producers in parallel.

That should just shut down the channel in [the only or the last one remaining] process of producer associations to inform consumers have no value can continue to read. As long as adhere to this principle, you can ensure that data is transmitted to a channel that has been closed can not happen.

Violence correct method of closing a channel

If you want to turn off at the consumer end of the channel or turn off the channel in multiple producers side, you can use a recovery mechanism to the last insurance program because avoid panic and collapse.

func SafeClose(ch chan T) (justClosed bool) {
    defer func() {
        if recover() != nil {
            justClosed = false
        }
    }()
    
    // assume ch != nil here.
    close(ch) // panic if ch is closed
    return true // <=> justClosed = true; return
}

Using this method is clearly contrary to the above principle of closed channel, then performance can be, after all, will only be called once for each coroutine SafeClose, the performance loss is very small.

The same method can also be used to restore production at the time of the message.

func SafeSend(ch chan T, value T) (closed bool) {
    defer func() {
        if recover() != nil {
            // The return result can be altered 
            // in a defer function call.
            closed = true
        }
    }()
    
    ch <- value // panic if ch is closed
    return false // <=> closed = false; return
}

Close method polite channel

There are a lot of people often use with sync.Once to close the channel, which ensures that only close once

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

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

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

Similarly, we can also use sync.Mutex achieve the same purpose.

type MyChannel struct {
    C      chan T
    closed bool
    mutex  sync.Mutex
}

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

func (mc *MyChannel) SafeClose() {
    mc.mutex.Lock()
    if !mc.closed {
        close(mc.C)
        mc.closed = true
    }
    mc.mutex.Unlock()
}

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

To know golang designers do not provide SafeClose or SafeSend method for a reason, they should not have recommended in the consumer end or closed channels in multiple concurrent production side, such as closing a read-only channel in syntax to completely ban the use of .

Elegant method of closing a channel

SafeSend above methods a great disadvantage that it can not be used in the selected block, the statement. And another very important disadvantage is that people like me got a little older code, the use of panic / restore and synchronization / mutual exclusion to get not so elegant. Here we introduce a purely elegant solution under different scenarios can be used.

Multiple consumers, individual producers. This is the simplest case, the producer directly to close the channel better.

package main

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

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)
    
    // the sender
    go func() {
        for {
            if value := rand.Intn(MaxRandomNumber); value == 0 {
                // The only sender can close the channel safely.
                close(dataCh)
                return
            } else {            
                dataCh <- value
            }
        }
    }()
    
    // receivers
    for i := 0; i < NumReceivers; i++ {
        go func() {
            defer wgReceivers.Done()
            
            // Receive values until dataCh is closed and
            // the value buffer queue of dataCh is empty.
            for value := range dataCh {
                log.Println(value)
            }
        }()
    }
    
    wgReceivers.Wait()
}

Multiple producer, single consumer. This situation is a bit more complex than the above. We can not close the channel at the consumer end, which are contrary to the principles of the channel is closed. But we can make the consumer side closed an additional signal to notify the sender to stop production data.

package main

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

func main() {
    rand.Seed(time.Now().UnixNano())
    log.SetFlags(0)
    
    // ...
    const MaxRandomNumber = 100000
    const NumSenders = 1000
    
    wgReceivers := sync.WaitGroup{}
    wgReceivers.Add(1)
    
    // ...
    dataCh := make(chan int, 100)
    stopCh := make(chan struct{})
    // stopCh is an additional signal channel.
    // Its sender is the receiver of channel dataCh.
    // Its reveivers are the senders of channel dataCh.
    
    // senders
    for i := 0; i < NumSenders; i++ {
        go func() {
            for {
                // The first select here is to try to exit the goroutine
                // as early as possible. In fact, it is not essential
                // for this example, so it can be omitted.
                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):
                }
            }
        }()
    }
    
    // the receiver
    go func() {
        defer wgReceivers.Done()
        
        for value := range dataCh {
            if value == MaxRandomNumber-1 {
                // The receiver of the dataCh channel is
                // also the sender of the stopCh cahnnel.
                // It is safe to close the stop channel here.
                close(stopCh)
                return
            }
            
            log.Println(value)
        }
    }()
    
    // ...
    wgReceivers.Wait()
}

To the above example, the producer also exit the receiver signal channels, exits the channel signal is still closed by the end of its production, so it still does not violate the principles of closed channel. It is noted that, in this example the production and sink are not closed channel message data channel in the absence of any reference goroutine turn itself off, without the need to shut down the display.

More producers, more consumers

This is the most complex case, we can not allow neither allow the receiving end to close the channel transmitting end. We do not even allow recipients to close an exit signal to inform the producers to stop production. Because we can not violate the principle of closed channel. However, we can introduce an additional coordinator to close additional exit signal path.

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

We can not cover all the above three scenarios, but they are most common and versatile three scenarios, essentially all of the above scenarios can be divided into three categories.

in conclusion

No scene worthy of you to break the channel closed principle, if you encounter such a special scene, it is recommended that you think carefully about their own design, is not that what the reconstruction.

Reproduced in: https: //www.jianshu.com/p/c42eab69b382

Guess you like

Origin blog.csdn.net/weixin_34279579/article/details/91051980