How should a long-running worker loop respect cancellation? Discuss select fairness and CPU-bound loops.
Question 281MediumGo 1.22 to 1.25
For channel-driven loops, put ctx.Done() in the same select as the work channel. For CPU-bound loops that never block, check ctx.Err() regularly. Checking every N iterations keeps the overhead negligible: Err() is cheap, but it is a synchronized read (historically a mutex) that you don't want on every iteration of a hot loop.
func consume(ctx context.Context, jobs <-chan Job) error {
for {
select {
case <-ctx.Done():
return context.Cause(ctx)
case j, ok := <-jobs:
if !ok {
return nil
}
if err := ctx.Err(); err != nil { // priority check: see below
return err
}
handle(ctx, j)
}
}
}
func crunch(ctx context.Context, data []float64) (float64, error) {
var sum float64
for i := range len(data) {
if i%4096 == 0 {
if err := ctx.Err(); err != nil {
return 0, err
}
}
sum += math.Sqrt(data[i])
}
return sum, nil
}
Fairness gotcha: when several select cases are ready, Go picks one at random. With a busy jobs channel and a canceled context, the loop can keep taking jobs for several more iterations. If cancellation must win, re-check ctx.Err() after receiving, as above.
Also, never use select { case <-ctx.Done(): ... default: } as a spin loop without doing work in it. That just burns CPU.
More on Context
- Q279How does context cancellation work with database/sql, including transactions and rows?
- Q280Write a function that runs a blocking call with a context and returns early on cancellation. What goroutine leak must you avoid?
- Q282How does errgroup.WithContext use context, and what is a common bug with it?
- Q283How would you merge two contexts so the result is canceled when either one is?
- Q284Can you implement your own Context type? How does the stdlib propagate cancellation from a custom parent?
- Q285How do deadlines propagate across service boundaries (e.g., gRPC), and how do you manage a time budget across multiple downstream calls?