Skip to content

Commit 0ad3686

Browse files
committed
context: use fewer goroutines in WithCancel/WithTimeout
If the parent context passed to WithCancel or WithTimeout is a known context implementation (one created by this package), we attach the child to the parent by editing data structures directly; otherwise, for unknown parent implementations, we make a goroutine that watches for the parent to finish and propagates the cancellation. A common problem with this scheme, before this CL, is that users who write custom context implementations to manage their value sets cause WithCancel/WithTimeout to start goroutines that would have not been started before. This CL changes the way we map a parent context back to the underlying data structure. Instead of walking up through known context implementations to reach the *cancelCtx, we look up parent.Value(&cancelCtxKey) to return the innermost *cancelCtx, which we use if it matches parent.Done(). This way, a custom context implementation wrapping a *cancelCtx but not changing Done-ness (and not refusing to return wrapped keys) will not require a goroutine anymore in WithCancel/WithTimeout. For #28728. Change-Id: Idba2f435c81b19fe38d0dbf308458ca87c7381e9 Reviewed-on: https://go-review.googlesource.com/c/go/+/196521 Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent ae024d9 commit 0ad3686

File tree

4 files changed

+129
-17
lines changed

4 files changed

+129
-17
lines changed

src/context/context.go

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
"errors"
5252
"internal/reflectlite"
5353
"sync"
54+
"sync/atomic"
5455
"time"
5556
)
5657

@@ -239,11 +240,24 @@ func newCancelCtx(parent Context) cancelCtx {
239240
return cancelCtx{Context: parent}
240241
}
241242

243+
// goroutines counts the number of goroutines ever created; for testing.
244+
var goroutines int32
245+
242246
// propagateCancel arranges for child to be canceled when parent is.
243247
func propagateCancel(parent Context, child canceler) {
244-
if parent.Done() == nil {
248+
done := parent.Done()
249+
if done == nil {
245250
return // parent is never canceled
246251
}
252+
253+
select {
254+
case <-done:
255+
// parent is already canceled
256+
child.cancel(false, parent.Err())
257+
return
258+
default:
259+
}
260+
247261
if p, ok := parentCancelCtx(parent); ok {
248262
p.mu.Lock()
249263
if p.err != nil {
@@ -257,6 +271,7 @@ func propagateCancel(parent Context, child canceler) {
257271
}
258272
p.mu.Unlock()
259273
} else {
274+
atomic.AddInt32(&goroutines, +1)
260275
go func() {
261276
select {
262277
case <-parent.Done():
@@ -267,22 +282,31 @@ func propagateCancel(parent Context, child canceler) {
267282
}
268283
}
269284

270-
// parentCancelCtx follows a chain of parent references until it finds a
271-
// *cancelCtx. This function understands how each of the concrete types in this
272-
// package represents its parent.
285+
// &cancelCtxKey is the key that a cancelCtx returns itself for.
286+
var cancelCtxKey int
287+
288+
// parentCancelCtx returns the underlying *cancelCtx for parent.
289+
// It does this by looking up parent.Value(&cancelCtxKey) to find
290+
// the innermost enclosing *cancelCtx and then checking whether
291+
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
292+
// has been wrapped in a custom implementation providing a
293+
// different done channel, in which case we should not bypass it.)
273294
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
274-
for {
275-
switch c := parent.(type) {
276-
case *cancelCtx:
277-
return c, true
278-
case *timerCtx:
279-
return &c.cancelCtx, true
280-
case *valueCtx:
281-
parent = c.Context
282-
default:
283-
return nil, false
284-
}
295+
done := parent.Done()
296+
if done == closedchan || done == nil {
297+
return nil, false
298+
}
299+
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
300+
if !ok {
301+
return nil, false
285302
}
303+
p.mu.Lock()
304+
ok = p.done == done
305+
p.mu.Unlock()
306+
if !ok {
307+
return nil, false
308+
}
309+
return p, true
286310
}
287311

288312
// removeChild removes a context from its parent.
@@ -323,6 +347,13 @@ type cancelCtx struct {
323347
err error // set to non-nil by the first cancel call
324348
}
325349

350+
func (c *cancelCtx) Value(key interface{}) interface{} {
351+
if key == &cancelCtxKey {
352+
return c
353+
}
354+
return c.Context.Value(key)
355+
}
356+
326357
func (c *cancelCtx) Done() <-chan struct{} {
327358
c.mu.Lock()
328359
if c.done == nil {

src/context/context_test.go

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"runtime"
1111
"strings"
1212
"sync"
13+
"sync/atomic"
1314
"time"
1415
)
1516

@@ -21,6 +22,7 @@ type testingT interface {
2122
Failed() bool
2223
Fatal(args ...interface{})
2324
Fatalf(format string, args ...interface{})
25+
Helper()
2426
Log(args ...interface{})
2527
Logf(format string, args ...interface{})
2628
Name() string
@@ -401,7 +403,7 @@ func XTestAllocs(t testingT, testingShort func() bool, testingAllocsPerRun func(
401403
c, _ := WithTimeout(bg, 15*time.Millisecond)
402404
<-c.Done()
403405
},
404-
limit: 8,
406+
limit: 12,
405407
gccgoLimit: 15,
406408
},
407409
{
@@ -648,3 +650,81 @@ func XTestDeadlineExceededSupportsTimeout(t testingT) {
648650
t.Fatal("wrong value for timeout")
649651
}
650652
}
653+
654+
type myCtx struct {
655+
Context
656+
}
657+
658+
type myDoneCtx struct {
659+
Context
660+
}
661+
662+
func (d *myDoneCtx) Done() <-chan struct{} {
663+
c := make(chan struct{})
664+
return c
665+
}
666+
667+
func XTestCustomContextGoroutines(t testingT) {
668+
g := atomic.LoadInt32(&goroutines)
669+
checkNoGoroutine := func() {
670+
t.Helper()
671+
now := atomic.LoadInt32(&goroutines)
672+
if now != g {
673+
t.Fatalf("%d goroutines created", now-g)
674+
}
675+
}
676+
checkCreatedGoroutine := func() {
677+
t.Helper()
678+
now := atomic.LoadInt32(&goroutines)
679+
if now != g+1 {
680+
t.Fatalf("%d goroutines created, want 1", now-g)
681+
}
682+
g = now
683+
}
684+
685+
_, cancel0 := WithCancel(&myDoneCtx{Background()})
686+
cancel0()
687+
checkCreatedGoroutine()
688+
689+
_, cancel0 = WithTimeout(&myDoneCtx{Background()}, 1*time.Hour)
690+
cancel0()
691+
checkCreatedGoroutine()
692+
693+
checkNoGoroutine()
694+
defer checkNoGoroutine()
695+
696+
ctx1, cancel1 := WithCancel(Background())
697+
defer cancel1()
698+
checkNoGoroutine()
699+
700+
ctx2 := &myCtx{ctx1}
701+
ctx3, cancel3 := WithCancel(ctx2)
702+
defer cancel3()
703+
checkNoGoroutine()
704+
705+
_, cancel3b := WithCancel(&myDoneCtx{ctx2})
706+
defer cancel3b()
707+
checkCreatedGoroutine() // ctx1 is not providing Done, must not be used
708+
709+
ctx4, cancel4 := WithTimeout(ctx3, 1*time.Hour)
710+
defer cancel4()
711+
checkNoGoroutine()
712+
713+
ctx5, cancel5 := WithCancel(ctx4)
714+
defer cancel5()
715+
checkNoGoroutine()
716+
717+
cancel5()
718+
checkNoGoroutine()
719+
720+
_, cancel6 := WithTimeout(ctx5, 1*time.Hour)
721+
defer cancel6()
722+
checkNoGoroutine()
723+
724+
// Check applied to cancelled context.
725+
cancel6()
726+
cancel1()
727+
_, cancel7 := WithCancel(ctx5)
728+
defer cancel7()
729+
checkNoGoroutine()
730+
}

src/context/x_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ func TestCancelRemoves(t *testing.T) { XTestCancelRemoves(t) }
2727
func TestWithCancelCanceledParent(t *testing.T) { XTestWithCancelCanceledParent(t) }
2828
func TestWithValueChecksKey(t *testing.T) { XTestWithValueChecksKey(t) }
2929
func TestDeadlineExceededSupportsTimeout(t *testing.T) { XTestDeadlineExceededSupportsTimeout(t) }
30+
func TestCustomContextGoroutines(t *testing.T) { XTestCustomContextGoroutines(t) }

src/go/build/deps_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ var pkgDeps = map[string][]string{
252252
"compress/gzip": {"L4", "compress/flate"},
253253
"compress/lzw": {"L4"},
254254
"compress/zlib": {"L4", "compress/flate"},
255-
"context": {"errors", "internal/reflectlite", "sync", "time"},
255+
"context": {"errors", "internal/reflectlite", "sync", "sync/atomic", "time"},
256256
"database/sql": {"L4", "container/list", "context", "database/sql/driver", "database/sql/internal"},
257257
"database/sql/driver": {"L4", "context", "time", "database/sql/internal"},
258258
"debug/dwarf": {"L4"},

0 commit comments

Comments
 (0)