background

In Go, goroutine creation cost is low and scheduling efficiency is high. It is not surprising that there are hundreds of thousands of Goroutine at the same time. Although the memory used by a single Goroutine is limited, it does not mean that there is no limit to creating goroutines.

Never start a goroutine without knowing how it will stop

Each time you start the Goroutine, you must know when and how the Goroutine exits. Otherwise, the program is potentially a memory leak. Before we talk about coroutines exiting, why do coroutines block

Coroutines blocking

Coroutine blocking cannot exit freely, mainly because of two things:

  • Timeout control
  • Process control

context

The former is easy to understand. When a Goroutine is started to process a transaction, it is expected that the transaction will complete at a certain time.

  • RPC call: The maximum timeout period does not exceed the waiting time of the user
  • Scheduled task: The execution time should not exceed the startup interval

For when to exit, Go provides a Context for Goroutine lifecycle management

  • Cancellation via context.WithCancel.

  • Timeout via context.WithDeadline.

    req, err := http.NewRequest("GET"."https://play.golang.org/".nil)
    iferr ! =nil {
    	log.Fatalf("%v", err)
    }
    
    ctx, cancel := context.WithTimeout(req.Context(), 1*time.Second)
    defer cancel()
    
    req = req.WithContext(ctx)
    client := http.DefaultClient
    resp, err := client.Do(req)
    iferr ! =nil {
    	log.Fatalf("%v", err)
    }
    fmt.Printf("%v\n", resp.StatusCode)
    Copy the code

channel & select

The latter is more difficult to understand. For speakers of other languages in particular, flow control in a program generally means:

  • if/else
  • for loop

In Go, a similar understanding is only half right. Because channel and SELECT are the focus of process control.

Channels provide a powerful ability to flow data from one Goroutine to another. It also means that a channel affects both the data flow and the control flow of a program.

  • A closed channel will never block

    package main
    
    import "fmt"
    
    func main(a) {
        ch := make(chan bool.2)
        ch <- true
        ch <- true
        close(ch)
    
        for v := range ch {
            fmt.Println(v) // called twice}}Copy the code
  • Nil channels always block

    package main
    
    func main(a) {
        var ch chan bool
        ch <- true // blocks forever
    }
    Copy the code
  • A buffered/unbuffered channel is somewhere in between and is blocked because of whether the channel can be read or written

So how do you tell if a channel is read or write? The answer is select.

select{
case channel_send_or_receive:
    //Dosomething
case channel_send_or_receive:
    //Dosomething
default:
    //Dosomething
}
Copy the code

Coroutines exit

With all that said, how do coroutines exit? I believe it is easy to get the conclusion from the above part:

  • Timeout return
  • Returns according to the channel readable status
// Method 1: traversal the closed channel
for x := range closedCh {
    fmt.Printf("Process %d\n", x)
}
// Method 2: Select a readable channel
for {
    select {
        case <-stopCh:
            fmt.Println("Recv stop signal")
            return
         case <-t.C:
            fmt.Println("Working .")}}Copy the code

Perfect exit

Is it enough that coroutines can exit? The perfect exit should include three things:

  • Notifying coroutine to exit
  • Notification confirmation, coroutine exits
  • Gets the error that the coroutine eventually returns

For example: ErrGroup

func (g *Group) Wait(a) error {
    g.wg.Wait()
    ifg.cancel ! =nil {
        g.cancel()
    }
    return g.err
}
Copy the code

This article is written by Cyningsun at www.cyningsun.com/01-31-2021/… Copyright Notice: All articles in this blog are under the CC BY-NC-ND 3.0CN license unless otherwise stated. Reprint please indicate the source!