Reputation: 981
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
go publishMetrics(ctx)
In my understanding, when the cancelFunc()
is invoked, context is cancelled and the go routine receives the signal, but is it allowed for the routine to do some cleanup work? For example, there might be unpublished metrics in memory, the background routine might try to upload them before exiting, I want to give the background go routine sometime to cleanup when context is cancelled, for example, wait for 100ms and if the go routine can upload within 100ms, that is good, if not, just exit, is there a way to achieve the goal? Thanks.
Upvotes: 1
Views: 1136
Reputation: 18290
when the
cancelFunc()
is invoked, context is cancelled and the go routine receives the signal, but is it allowed for the routine to do some cleanup work?
The simple answer is yes. Calling cancelFunc()
provides a cancellation signal to the goroutine via the Context
. The goroutine can:
This means that if your goroutine is performing a standalone task (e.g. publishing metrics!) you can just cancel the context (call cancelFunc()
) and continue doing whatever you need to (if the goroutine is well behaved it will terminate in a timely manner).
However often you do need to know when the the goroutine has completed it's processing (cleanup etc). One common situation is handling program termination. When main
completes the program exits (the runtime will not wait for goroutines to complete), so if you want to allow a goroutine to perform cleanup work, then you need to wait for this.
It's important to note that this is two separate operations:
Context
or another means) that the goroutine should terminate.An example of this follows (playground); the gracePeriod
configures how long the application will wait for the goroutine to complete before exiting:
func main() {
gracePeriod := 800 * time.Millisecond // decrease to, say 200, to observe exit without waiting for completion
// ctx, cancel = signal.NotifyContext(context.Background(), os.Interrupt) // real app would wait for an interupt signal from the operating system
ctx, cancel := context.WithCancel(context.Background()) // we simulate this by valling cancel() below
defer cancel() // not really needed here but good practice (easy to forget to cancel a context in some paths)
done := make(chan struct{}) // This will be used to signal when the goroutine is complete
go func() {
defer close(done)
publishMetrics(ctx)
}()
// At some point we receive a cancellation signal - lets simulate that
time.Sleep(time.Millisecond)
cancel()
select {
case <-done:
fmt.Println("Terminating regularly")
case <-time.After(gracePeriod): // after gracePeriod we exit regardless of the goroutine state
fmt.Println("Timed out waiting; exiting without waiting any longer")
}
}
func publishMetrics(ctx context.Context) {
// Lets assume that this function runs until it receives a signal (at which point it tidies up and exits)
for {
// Do work
time.Sleep(10 * time.Millisecond)
// Using ctx.Err to detect when the context has been cancelled; often you would use `ctx.Done()` in a `select` whilst waiting for something else
if ctx.Err() != nil {
break
}
}
// Simulate cleanup
fmt.Println("goroutine - cancel signalled")
time.Sleep(500 * time.Millisecond)
fmt.Println("goroutine - cleanup done")
}
Note: The other answers discuss context.WithTimeout
; this is generally used to signal that an operation should be cancelled if it does not complete within the specified time. In other words it sends a delayed signal; this is different from waiting for the goroutine to process the signal and perform whatever cleanup is needed.
Upvotes: 0
Reputation: 4610
Goroutines do not receive signals. The only thing that cancelling a context does is - cancelling this context. See an example of a cancellable goroutine - it has to either do
select {
case <-ctx.Done():
// handle cacellation
// ... do work
on the passed context or check ctx.Err()
, see also this blog post about cancellation. Even in Java threads have to check Thread.interrupted()
and Thread.stop
is deprecated.
If you want to delay the cancellation signal for derived contexts you have to break the cancellation chain with WithoutCancel
and build your own. I would advise against it, because the semantics would be unclear - something timed out, but you want to add 100mS to this timeout - and the next routine might add 100mS too, and so on...
When you exit a program all goroutines are terminated immediately, like daemon threads in Java. So you might want to signal “upload unpublished metrics” immediately and be willing to wait 100mS for completion before exiting (Go Playground):
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
exitCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
uploadDone := make(chan struct{})
go func() {
defer close(uploadDone)
publishMetrics(exitCtx)
}()
select {
case <-uploadDone:
fmt.Println("Terminating regularly")
case <-exitCtx.Done():
fmt.Println("Timed out")
}
}
func publishMetrics(ctx context.Context) {
timer := time.NewTimer(500 * time.Millisecond) // Simulate upload
select {
case <-timer.C:
fmt.Println("Upload successful")
case <-ctx.Done():
timer.Stop()
fmt.Println("Upload aborted")
}
}
Upvotes: 1
Reputation: 9
For doing some cleanup work, you can use
context.WithTimeout(context.Background(), N * time.Second)
where N = time (in seconds) after which the created context will be timed out
instead of using context.WithCancel(context.Background())
.
For example, if you want to wait for 100 ms (= 0.1 s), then you may write
ctx, cancelFunc := context.WithTimeout(context.Background(), 0.1 * time.Second)
The context created in the above method will be automatically cancelled after the time-out period (in seconds).
Another way is modifying your code in a way like this, where you create a new sub-routine:
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
time.Sleep(0.1 * time.Second)
cancelFunc()
}
Upvotes: 0