Explain how cancellation propagates through a context tree. What happens internally when a parent is canceled?
Question 260HardGo 1.22 to 1.25
Each cancelable context (*cancelCtx) holds a mutex, a lazily created done channel, an err, a cause, and a children map[canceler]struct{}. When you derive a cancelable child, propagateCancel runs:
- If the parent is already done, the child is canceled at once with the parent's error and cause.
- If the parent is (or wraps) a stdlib
*cancelCtx, the child is added to the parent'schildrenmap. No goroutine is needed. - If the parent is a custom type that has an
AfterFuncmethod (Go 1.21+), that method is used to register the child. - Otherwise a goroutine is started that waits on
parent.Done()andchild.Done().
When cancel runs, it sets err and cause, closes done, cancels every child recursively, and removes itself from its parent's map. Canceling is idempotent: only the first call takes effect.
root, cancel := context.WithCancel(context.Background())
a, cancelA := context.WithCancel(root)
b, cancelB := context.WithTimeout(a, time.Minute)
defer cancelA()
defer cancelB()
cancel() // closes root.Done, then a.Done, then b.Done
<-b.Done()
fmt.Println(b.Err()) // context canceled (inherited from root)
Gotcha: canceling a child never affects its parent or siblings.
More on Context
- Q259What problem does context.Context solve, and what are its four methods?
- Q261Why must you always call the CancelFunc, even if the operation finished successfully? What leaks if you don't?
- Q262WithTimeout vs WithDeadline: what is the difference, and what happens when a child asks for a later deadline than its parent?
- Q263What does this print?
- Q264What values can ctx.Err() return, and how should you check them?
- Q265What is WithCancelCause, and how does ctx.Err() differ from context.Cause(ctx)?