diff --git a/src/context/benchmark_test.go b/src/context/benchmark_test.go index d10950d258970f..e4b37f11518592 100644 --- a/src/context/benchmark_test.go +++ b/src/context/benchmark_test.go @@ -208,3 +208,42 @@ func BenchmarkErrCanceled(b *testing.B) { } } } + +func BenchmarkErrOKParallel(b *testing.B) { + ctx, cancel := WithCancel(Background()) + defer cancel() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := ctx.Err(); err != nil { + b.Fatalf("ctx.Err() = %v", err) + } + } + }) +} + +func BenchmarkErrCanceledParallel(b *testing.B) { + ctx, cancel := WithCancel(Background()) + cancel() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if err := ctx.Err(); err == nil { + b.Fatalf("ctx.Err() = %v", err) + } + } + }) +} + +// BenchmarkErrCanceledParallelDistinct gives each goroutine its own canceled +// context, so the contexts share nothing with each other. Any contention here +// is contention on state shared by every canceled context in the program. +func BenchmarkErrCanceledParallelDistinct(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + ctx, cancel := WithCancel(Background()) + cancel() + for pb.Next() { + if err := ctx.Err(); err == nil { + b.Fatalf("ctx.Err() = %v", err) + } + } + }) +} diff --git a/src/context/context.go b/src/context/context.go index 5c3fe8dec6967a..da596d3c6c1d4e 100644 --- a/src/context/context.go +++ b/src/context/context.go @@ -465,7 +465,11 @@ func (c *cancelCtx) Err() error { // An atomic load is ~5x faster than a mutex, which can matter in tight loops. if err := c.err.Load(); err != nil { // Ensure the done channel has been closed before returning a non-nil error. - <-c.Done() + // closedchan is closed at init and shared by every canceled context, so + // receiving from it would take one process-global lock for nothing. + if done := c.Done(); done != closedchan { + <-done + } return err.(error) } return nil