Home > Software design >  Is it possible to cancel unfinished goroutines?
Is it possible to cancel unfinished goroutines?

Time:04-14

Consider a group of check works, each of which has independent logic, so they seem to be good to run concurrently, like:

type Work struct {
    // ...
}

// This Check could be quite time-consuming
func (w *Work) Check() bool {
    // return succeed or not

    //...
}

func CheckAll(works []*Work) {
    num := len(works)
    results := make(chan bool, num)
    for _, w := range works {
        go func(w *Work) {
            results <- w.Check()
        }(w)
    }

    for i := 0; i < num; i   {
        if r := <-results; !r {
            ReportFailed()
            break;
        }
    }
}

func ReportFailed() {
    // ...
}

When concerned about the results, if the logic is no matter which one work fails, we assert all works totally fail, the remaining values in the channel are useless. Let the remaining unfinished goroutines continue to run and send results to the channel is meaningless and waste, especially when w.Check() is quite time-consuming. The ideal effect is similar to:

    for _, w := range works {
        if !w.Check() {
            ReportFailed()
            break;
        }
    }

This only runs necessary check works then break, but is in sequential non-concurrent scenario.

So, is it possible to cancel these unfinished goroutines, or sending to channel?

CodePudding user response:

Cancelling a (blocking) send

Your original question asked how to cancel a send operation. A send on a channel is basically "instant". A send on a channel blocks if the channel's buffer is full and there is no ready receiver.

You can "cancel" this send by using a select statement and a cancel channel which you close, e.g.:

cancel := make(chan struct{})

select {
case ch <- value:
case <- cancel:
}

Closing the cancel channel with close(cancel) on another goroutine will make the above select abandon the send on ch (if it's blocking).

But as said, the send is "instant" on a "ready" channel, and the send first evaluates the value to be sent:

results <- w.Check()

This first has to run w.Check(), and once it's done, its return value will be sent on results.

Cancelling a function call

So what you really need is to cancel the w.Check() method call. For that, the idiomatic way is to pass a context.Context value which you can cancel, and w.Check() itself must monitor and "obey" this cancellation request.

See Terminating function execution if a context is cancelled

Note that your function must support this explicitly. There is no implicit termination of function calls or goroutines, see cancel a blocking operation in Go.

So your Check() should look something like this:

// This Check could be quite time-consuming
func (w *Work) Check(ctx context.Context, workDuration time.Duration) bool {
    // Do your thing and monitor the context!

    select {
    case <-ctx.Done():
        return false
    case <-time.After(workDuration): // Simulate work
        return true
    case <-time.After(2500 * time.Millisecond): // Simulate failure after 2.5 sec
        return false
    }
}

And CheckAll() may look like this:

func CheckAll(works []*Work) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    num := len(works)
    results := make(chan bool, num)

    wg := &sync.WaitGroup{}
    for i, w := range works {
        workDuration := time.Second * time.Duration(i)
        wg.Add(1)
        go func(w *Work) {
            defer wg.Done()
            result := w.Check(ctx, workDuration)
            // You may check and return if context is cancelled
            // so result is surely not sent, I omitted it here.
            select {
            case results <- result:
            case <-ctx.Done():
                return
            }
        }(w)
    }

    go func() {
        wg.Wait()
        close(results) // This allows the for range over results to terminate
    }()

    for result := range results {
        fmt.Println("Result:", result)
        if !result {
            cancel()
            break
        }
    }
}

Testing it:

CheckAll(make([]*Work, 10))

Output (try it on the Go Playground):

Result: true
Result: true
Result: true
Result: false

We get true printed 3 times (works that complete under 2.5 seconds), then the failure simulation kicks in, returns false, and terminates all other jobs.

Note that the sync.WaitGroup in the above example is not strictly needed as results has a buffer capable of holding all results, but in general it's still good practice (should you use a smaller buffer in the future).

See related: Close multiple goroutine if an error occurs in one in go

CodePudding user response:

package main

import "fmt"

type Work struct {
    // ...
    Name string
    IsSuccess chan bool
}

// This Check could be quite time-consuming
func (w *Work) Check() {
    // return succeed or not

    //...
    if len(w.Name) > 0 {
        w.IsSuccess <- true
    }else{
        w.IsSuccess <- false
    }

}


//堆排序
func main() {
    works := make([]*Work,3)
    works[0] = &Work{
        Name: "",
        IsSuccess: make(chan bool),
    }
    works[1] =  &Work{
        Name: "111",
        IsSuccess: make(chan bool),
    }
    works[2] =&Work{
        Name: "",
        IsSuccess: make(chan bool),
    }

    for _,w := range works {
        go w.Check()
    }

    for i,w := range works{
        select {
        case checkResult := <-w.IsSuccess :
            fmt.Printf("index %d checkresult %t \n",i,checkResult)
        }
    }
}

enter image description here

CodePudding user response:

The short answer is: No.

You can not cancel or close any goroutine unless the goroutine itself reaches the return or end of its stack.

If you want to cancel something, the best approach is to pass a context.Context to them and listen to this context.Done() inside of the routine. Whenever context is canceled, you should return and the goroutine will automatically die after executing defers(if any).

  • Related