Search code examples
gogoroutinego-context

Context cancelled by timeout but computation is not aborted?


Trying to understand how go context cancellation will abort execution of subsequent code

Details of experiment:

  1. main func has a context that times out in 2sec
  2. main func calls another func sum in a separate go-routine - which sleeps for 1sec for test-run-1 & 4sec for test-run-2
  3. letting main sleep for 3sec to let spun go-routine complete execution
package main

import (
    "context"
    "fmt"
    "log"
    "time"
)

func main() {

    c := context.Background()
    childCtx, cancel := context.WithTimeout(c, 2*time.Second)
    defer cancel()

    ch := make(chan int, 1)
    go sum(5, 6, ch)

    var msg string

    select {
    case <-childCtx.Done():
        msg = "return from ctx done channel"
    case res := <-ch:
        msg = fmt.Sprintf("return from go routine: %v", res)
    }

    log.Print(msg)

    time.Sleep(3 * time.Second) //sleeping here to test if go-routine is still running
}


func sum(x int, y int, c chan<- int) {
    time.Sleep(1 * time.Second) 
    //testcase-1: sleep - 1s
    //testcase-2: sleep - 4s

    result := x + y

    log.Printf("print from sum fn: %v", result)

    c <- result
}

Response for testcase-1 : sleep sum function for 1 sec:

2021/04/12 01:06:58 print from sum fn: 11
2021/04/12 01:06:58 return from go routine: 11

Response for testcase-2 : sleep sum function for 4 sec:

2021/04/12 01:08:25 return from ctx done channel
2021/04/12 01:08:27 print from sum fn: 11

In testcase-2 when sum func sleeps for 4 secs, context is already cancelled by timeout after 2secs, why is it still executing the sum func in diff go-routine and printing print from sum fn: 1 ?

From documentation: Canceling this context releases resources associated with it.

My assumption is that all the computation will be aborted immediately after 2 secs including the spun go-routine

Let me know how to do this right, thanks in adavance


Solution

  • As @AndySchweig has noted, context signals a cancelation event, but does not enforce cancelation. It's up to any potentially blocking goroutine to do its best at trying to cancel/clean-up after it detects a cancelation.

    To update your sum function to support cancelation you could try:

    // add context parameter as the first argument
    // add a return error - to indicate any errors (i.e. function was interrupted due to cancelation)
    func sum(ctx context.Context, x int, y int, c chan<- int) (err error) {
    
        wait := 1 * time.Second // testcase-1
        //wait := 4 * time.Second // testcase-2
    
        // any blocking called - even sleeps - should be interruptible
        select {
        case <-time.After(wait):
        case <-ctx.Done():
            err = ctx.Err()
            return
        }
    
        result := x + y
    
        log.Printf("print from sum fn: %v", result)
    
        select {
        case c <- result:
        case <-ctx.Done(): // check for ctx cancelation here - as no one may be listening on result channel
            err = ctx.Err()
        }
        return
    }
    

    https://play.golang.org/p/DuIACxPvHYJ