Go中的父子上下文取消顺序

I want to know if there are any guarantees regarding the return order upon Context cancellation in golang.

I want to create a context with cancellation and once all the listeners are done with processing catching and reacting to "<-ctx.Done()" from this context, I want to call os.Exit safely.

A concrete example to explain the idea of what I want is following. I want to catch a signal, trigger all cancellations, and then call os.Exit().

I create a context and listen for a signal:

ctx, cancel := context.WithCancel(context.Background())
go func() {
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt)
    defer signal.Stop(c)

    select {
    case <-c:
        cancel()
    }
}()

In other places I "sign up" for this request several times:

res := NewRes()
go func() {
    <-ctx.Done():
    res.Close()
}()

But then I want to call os.Exit at the point when all the listeners are done.

For that I plan to create either parent or child context like this:

parent, pCancel := context.WithCancel(context.Background())
child, _ := context.WithCancel(parent)
go func() {
    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt)
    defer signal.Stop(c)

    select {
    case <-c:
        pCancel()
    case <-child.Done():
        os.Exit(0)
    }
}()

Unfortunately, I did not find the documentation describing the order how context are canceled, so I cannot come up with the correct solution for now.

You have to wait all routines before exiting. Calling pCancel() doesn't mean everything will stop. I recommend to do in routine all jobs, but on the main thread to wait for os.Interrupt signal.

Check example below

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "sync"
    "time"
)

func main() {
    parent, pCancel := context.WithCancel(context.Background())
    child, _ := context.WithCancel(parent)
    wg := &sync.WaitGroup{}

    for i := 0; i < 10; i++ {
        go work(wg, child)
    }

    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt)
    defer signal.Stop(c)

    select {
    case <-c:
        pCancel()
        fmt.Println("Waiting everyone to finish...")
        wg.Wait()
        fmt.Println("Exiting")
        os.Exit(0)
    }
}

func work(wg *sync.WaitGroup, ctx context.Context) {
    done := false
    wg.Add(1)
    for !done {
        fmt.Println("Doing something...")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("Done")
            done = true
        default:

        }
    }
    wg.Done()
}

Although, It's recommended to use principle "Share Memory By Communicating". Here is another example without using WaitGroup.

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "time"
)

func main() {
    parent, pCancel := context.WithCancel(context.Background())
    child, _ := context.WithCancel(parent)
    done := make(chan struct{})
    jobsCount := 10

    for i := 0; i < jobsCount; i++ {
        go work(child, done)
    }

    c := make(chan os.Signal)
    signal.Notify(c, os.Interrupt)
    defer signal.Stop(c)

    select {
    case <-c:
        pCancel()
        fmt.Println("Waiting everyone to finish...")
        for i := 0; i < jobsCount; i++ {
            <-done
        }
        fmt.Println("Exiting")
        os.Exit(0)
    }
}

func work(ctx context.Context, doneChan chan struct{}) {
    done := false
    for !done {
        fmt.Println("Doing something...")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("Done")
            done = true
        default:

        }
    }
    doneChan <- struct{}{}
}