One cancel reaches every goroutine¶
Level: 201 · anyone who has started goroutines and now has to stop them
One line: Calling a context's cancel closes ctx.Done() for every goroutine holding that context or one derived from it, and each goroutine waiting on it returns, with ctx.Err() now context.Canceled; cancelling a child never reaches its parent.
A goroutine cannot be stopped from outside (A goroutine has no handle). It can be asked. A context's Done method returns a channel that is closed when the work done for that context should stop, and a receive on a closed channel always proceeds at once ↗, so a single close wakes every goroutine waiting on it, however many there are.
The program below builds a tree of contexts for one order: payment and shipping are derived from order, and label is derived from shipping. It starts one goroutine per context, each waiting on its own Done, and then cancels twice: the shipping branch first, then the whole order.
Verified output of cancel_reaches_every_goroutine_go.go — regenerated by tools/run_examples.py, never hand-typed.
before any cancel:
order ctx.Err() = <nil> goroutine waiting
payment ctx.Err() = <nil> goroutine waiting
shipping ctx.Err() = <nil> goroutine waiting
label ctx.Err() = <nil> goroutine waiting
after cancelShipping():
order ctx.Err() = <nil> goroutine waiting
payment ctx.Err() = <nil> goroutine waiting
shipping ctx.Err() = context canceled goroutine returned
label ctx.Err() = context canceled goroutine returned
after cancelOrder():
order ctx.Err() = context canceled goroutine returned
payment ctx.Err() = context canceled goroutine returned
shipping ctx.Err() = context canceled goroutine returned
label ctx.Err() = context canceled goroutine returned
errors.Is(payment.Err(), context.Canceled): true
Reading the output¶
Before any cancel, every Err is nil and every goroutine is waiting. The Context ↗ interface promises that Err returns nil until Done is closed, and a non-nil error that never changes after it.
cancelShipping() closed two channels, shipping's and label's, because label was derived from shipping. order and payment did not notice. A cancel travels down the tree, never up to a parent and never across to a sibling. The word waiting in those rows is not a bet on timing: each of those goroutines returns only once its Done is closed, and theirs are not.
cancelOrder() closed the rest. Nothing had called cancelPayment by then, and payment's goroutine returned anyway: the package overview ↗ states that when a context is canceled, all contexts derived from it are canceled too. The last line shows the test to use on the error, errors.Is(err, context.Canceled); the next lesson has the other error a context can end with.
Two things the output does not show, both from the documentation for CancelFunc ↗. A cancel does not wait for the work to stop, which is why the program waits for its goroutines itself, with a channel per step and a sync.WaitGroup (A WaitGroup counts goroutines). And after the first call, calling a cancel again does nothing, which is why every defer cancel...() in the program is safe even for a context that was already canceled.
The goroutines here only wait. Real work waits on its own channels as well, in a select with one more case for <-ctx.Done() (select waits on many channels), and returns ctx.Err() from that case. A goroutine that never looks at Done is not stopped by any cancel.
cancel_reaches_every_goroutine_go.go
cancel_reaches_every_goroutine_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// One cancel() reaches every goroutine that holds the context, or a context
// derived from it, and each of them returns. Cancelling a child never reaches
// its parent.
//
// go build cancel_reaches_every_goroutine_go.go && ./cancel_reaches_every_goroutine_go
package main
import (
"context"
"errors"
"fmt"
"sync"
)
// One goroutine per step of an order, each holding its own context:
//
// order
// ├── payment
// └── shipping
// └── label
type step struct {
name string
ctx context.Context
returned chan struct{} // closed when the step's goroutine returns
}
func main() {
order, cancelOrder := context.WithCancel(context.Background())
defer cancelOrder()
payment, cancelPayment := context.WithCancel(order)
defer cancelPayment()
shipping, cancelShipping := context.WithCancel(order)
defer cancelShipping()
label, cancelLabel := context.WithCancel(shipping)
defer cancelLabel()
steps := []*step{
{name: "order", ctx: order},
{name: "payment", ctx: payment},
{name: "shipping", ctx: shipping},
{name: "label", ctx: label},
}
var wg sync.WaitGroup
for _, s := range steps {
s.returned = make(chan struct{})
wg.Go(func() {
defer close(s.returned)
<-s.ctx.Done() // real work would select on this alongside its own channels
})
}
report("before any cancel", steps)
cancelShipping()
<-steps[2].returned
<-steps[3].returned
report("after cancelShipping()", steps)
cancelOrder()
wg.Wait()
report("after cancelOrder()", steps)
fmt.Println("errors.Is(payment.Err(), context.Canceled):", errors.Is(payment.Err(), context.Canceled))
}
// report prints each step's ctx.Err() and whether its goroutine has returned.
// A goroutine whose context is not canceled cannot have returned, so
// "waiting" is as certain as "returned".
func report(title string, steps []*step) {
fmt.Println(title + ":")
for _, s := range steps {
state := "waiting"
select {
case <-s.returned:
state = "returned"
default:
}
fmt.Printf(" %-9s ctx.Err() = %-17v goroutine %s\n", s.name, s.ctx.Err(), state)
}
}
Always call cancel¶
The package overview gives the reason to call a cancel even when the work finished on its own: calling it removes the child from its parent and stops any timer the context holds, and not calling it leaks the child, and its children, until the parent is canceled. For a child of context.Background(), which is never canceled, that is for the life of the program. go vet checks for it, with an analyzer named lostcancel that runs by default:
Verified output of cancel_never_called_vet_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
$ go vet .
./quote.go:10:7: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak
./quote.go:15:2: the cancel function is not used on all paths (possible context leak)
./quote.go:17:3: this return statement may be reached without using the cancel var defined on line 15
exit status 1
$ go vet . # after moving defer cancel() to the line after each context.With...
exit status 0
The first finding is a cancel thrown away with _. The second and third are one mistake: defer cancel() comes after an early return, so that path leaves without it. The second quote.go puts defer cancel() on the line after each context is made, and vet exits 0.
cancel_never_called_vet_sh.sh
cancel_never_called_vet_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.
#!/usr/bin/env bash
# go vet's lostcancel check. A CancelFunc that is thrown away, or called on only
# some of the paths out of a function, is reported, and vet exits 1. The same
# two functions with `defer cancel()` straight after each context is made pass.
set -u
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
export GOTOOLCHAIN=local
say() { printf '$ %s\n' "$*"; }
# vet names the package on lines that start with #; its findings are the rest.
vet() {
go vet . 2>"$dir/vet.txt"
local status=$?
grep -v '^#' "$dir/vet.txt"
echo "exit status $status"
}
cd "$dir" || exit 1
go mod init example 2>/dev/null
cat >quote.go <<'GO'
package main
import (
"context"
"errors"
"time"
)
func quoteDiscarded(parent context.Context) error {
ctx, _ := context.WithTimeout(parent, time.Minute)
return ctx.Err()
}
func quoteOnePath(parent context.Context, supplier string) error {
ctx, cancel := context.WithCancel(parent)
if supplier == "" {
return errors.New("no supplier")
}
defer cancel()
return ctx.Err()
}
func main() {
_ = quoteDiscarded(context.Background())
_ = quoteOnePath(context.Background(), "acme")
}
GO
say 'go vet .'
vet
cat >quote.go <<'GO'
package main
import (
"context"
"errors"
"time"
)
func quoteDiscarded(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, time.Minute)
defer cancel()
return ctx.Err()
}
func quoteOnePath(parent context.Context, supplier string) error {
ctx, cancel := context.WithCancel(parent)
defer cancel()
if supplier == "" {
return errors.New("no supplier")
}
return ctx.Err()
}
func main() {
_ = quoteDiscarded(context.Background())
_ = quoteOnePath(context.Background(), "acme")
}
GO
say 'go vet . # after moving defer cancel() to the line after each context.With...'
vet
What to do¶
- Give every goroutine you may need to stop a context, and make each of its blocking waits a
selectwith a<-ctx.Done()case that returnsctx.Err(). - Derive a child with
context.WithCancel(parent)for each part you may want to stop on its own. Cancelling the child stops that part; cancelling the parent still stops everything. - Write
defer cancel()on the line after the context is made, and rungo vet. - A cancel asks; it does not wait. When the caller needs the goroutines to have stopped, it waits for them. First error cancels the rest puts the two together.
In other languages¶
- The Concurrency library's Who waits when main returns? ↗ notes that Python's threading documentation recommends an ordinary thread and a signal such as an
Event↗ for work that has to stop cleanly. AnEventis the closest thing toDone: set once, seen by every thread that waits on it. It has no children, though, andclear()can unset it, while a canceled context stays canceled. - The same page's C++ section uses
std::jthread, whose destructor asks its thread to stop. The thread sees the request through astd::stop_token↗ and has to check it and return, just as a goroutine has to watchDone. - Java's
Thread.interrupt↗ is also a request: it sets a flag, and it wakes a thread blocked insleep,joinorwaitwith anInterruptedException. That wake-up is what aselectonctx.Done()gives a goroutine, but only a goroutine that asks for it. - The Rust library's Spawning a thread ↗: a
JoinHandle↗ offersjoin,threadandis_finished, and nothing that stops the thread. Stopping is a flag or a channel the thread checks, which is a context without the tree. - Cancellation in async code, where a task can be cancelled at an
await, belongs to the Concurrency library's planned chapter 06, Async.
Sources¶
contextpackage ↗ — the overview (derived contexts are canceled with their parent; call theCancelFunc;go vetchecks it),WithCancel↗,CancelFunc↗, and theDoneandErrmethods ofContext↗.- The Go Programming Language Specification: Receive operator ↗ — a receive on a closed channel proceeds immediately.
cmd/vet↗ — lists thelostcancelanalyzer;go tool vet help lostcanceldescribes it.- Go Concurrency Patterns: Context ↗ — Sameer Ajmani, the Go blog, 2014.
- Katherine Cox-Buday, Concurrency in Go (O'Reilly, 2017), chapter 4, "Concurrency Patterns in Go", section "The context Package".
- Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 14, "The Context", section "Cancellation".