runtime: fix interactions between synctest, race detector, and timers
When an AfterFunc executes in a synctest bubble, there is a series of
happens-before relationships:
1. The AfterFunc is created.
2. The AfterFunc goroutine executes.
3. The AfterFunc goroutine returns.
4. A subsequent synctest.Wait call returns.
We were failing to correctly establish the happens-before relationship
between the AfterFunc goroutine and the AfterFunc itself being created.
When an AfterFunc executes, the G running the timer temporarily switches
to the timer heap's racectx. It then calls time.goFunc, which starts a
new goroutine to execute the timer. time.goFunc relies on the new goroutine
inheriting the racectx of the G running the timer.
Normal, non-synctest timers, execute with m.curg == nil, which causes
new goroutines to inherit the g0 racectx. We were running synctest
timers with m.curg set (to the G executing synctest.Run), so the new
AfterFunc goroutine was created using m.curg's racectx. This resulted
in us not properly establishing the happens-before relationship between
AfterFunc being called and the AfterFunc goroutine starting.
Fix this by setting m.curg to nil while executing timers.
As one additional fix, when waking a blocked bubble, wake the root
goroutine rather than a goroutine blocked in Wait if there is a
timer that can fire.
Fixes #72750
Change-Id: I2b2d6b0f17f64649409adc93c2603f720494af89
Reviewed-on: https://go-review.googlesource.com/c/go/+/658595
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
diff --git a/src/internal/synctest/synctest_test.go b/src/internal/synctest/synctest_test.go
index 62acb42..010679b 100644
--- a/src/internal/synctest/synctest_test.go
+++ b/src/internal/synctest/synctest_test.go
@@ -455,28 +455,89 @@
}
func TestHappensBefore(t *testing.T) {
- var v int
+ // Use two parallel goroutines accessing different vars to ensure that
+ // we correctly account for multiple goroutines in the bubble.
+ var v1 int
+ var v2 int
synctest.Run(func() {
+ v1++ // 1
+ v2++ // 1
+
+ // Wait returns after these goroutines exit.
go func() {
- v++ // 1
+ v1++ // 2
}()
- // Wait creates a happens-before relationship on the above goroutine exiting.
+ go func() {
+ v2++ // 2
+ }()
synctest.Wait()
+
+ v1++ // 3
+ v2++ // 3
+
+ // Wait returns after these goroutines block.
+ ch1 := make(chan struct{})
go func() {
- v++ // 2
+ v1++ // 4
+ <-ch1
+ }()
+ go func() {
+ v2++ // 4
+ <-ch1
+ }()
+ synctest.Wait()
+
+ v1++ // 5
+ v2++ // 5
+ close(ch1)
+
+ // Wait returns after these timers run.
+ time.AfterFunc(0, func() {
+ v1++ // 6
+ })
+ time.AfterFunc(0, func() {
+ v2++ // 6
+ })
+ synctest.Wait()
+
+ v1++ // 7
+ v2++ // 7
+
+ // Wait returns after these timer goroutines block.
+ ch2 := make(chan struct{})
+ time.AfterFunc(0, func() {
+ v1++ // 8
+ <-ch2
+ })
+ time.AfterFunc(0, func() {
+ v2++ // 8
+ <-ch2
+ })
+ synctest.Wait()
+
+ v1++ // 9
+ v2++ // 9
+ close(ch2)
+ })
+ // This Run happens after the previous Run returns.
+ synctest.Run(func() {
+ go func() {
+ go func() {
+ v1++ // 10
+ }()
+ }()
+ go func() {
+ go func() {
+ v2++ // 10
+ }()
}()
})
- // Run exiting creates a happens-before relationship on goroutines started in the bubble.
- synctest.Run(func() {
- v++ // 3
- // There is a happens-before relationship between the time.AfterFunc call,
- // and the func running.
- time.AfterFunc(0, func() {
- v++ // 4
- })
- })
- if got, want := v, 4; got != want {
- t.Errorf("v = %v, want %v", got, want)
+ // These tests happen after Run returns.
+ if got, want := v1, 10; got != want {
+ t.Errorf("v1 = %v, want %v", got, want)
+ }
+ if got, want := v2, 10; got != want {
+ t.Errorf("v2 = %v, want %v", got, want)
}
}
diff --git a/src/runtime/race/testdata/synctest_test.go b/src/runtime/race/testdata/synctest_test.go
new file mode 100644
index 0000000..dfbd568
--- /dev/null
+++ b/src/runtime/race/testdata/synctest_test.go
@@ -0,0 +1,97 @@
+// Copyright 2025 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package race_test
+
+import (
+ "internal/synctest"
+ "testing"
+ "time"
+)
+
+func TestRaceSynctestGoroutinesExit(t *testing.T) {
+ synctest.Run(func() {
+ x := 0
+ _ = x
+ f := func() {
+ x = 1
+ }
+ go f()
+ go f()
+ })
+}
+
+func TestNoRaceSynctestGoroutinesExit(t *testing.T) {
+ synctest.Run(func() {
+ x := 0
+ _ = x
+ f := func() {
+ x = 1
+ }
+ go f()
+ synctest.Wait()
+ go f()
+ })
+}
+
+func TestRaceSynctestGoroutinesRecv(t *testing.T) {
+ synctest.Run(func() {
+ x := 0
+ _ = x
+ ch := make(chan struct{})
+ f := func() {
+ x = 1
+ <-ch
+ }
+ go f()
+ go f()
+ close(ch)
+ })
+}
+
+func TestRaceSynctestGoroutinesUnblocked(t *testing.T) {
+ synctest.Run(func() {
+ x := 0
+ _ = x
+ ch := make(chan struct{})
+ f := func() {
+ <-ch
+ x = 1
+ }
+ go f()
+ go f()
+ close(ch)
+ })
+}
+
+func TestRaceSynctestGoroutinesSleep(t *testing.T) {
+ synctest.Run(func() {
+ x := 0
+ _ = x
+ go func() {
+ time.Sleep(1 * time.Second)
+ x = 1
+ time.Sleep(2 * time.Second)
+ }()
+ go func() {
+ time.Sleep(2 * time.Second)
+ x = 1
+ time.Sleep(1 * time.Second)
+ }()
+ time.Sleep(5 * time.Second)
+ })
+}
+
+func TestRaceSynctestTimers(t *testing.T) {
+ synctest.Run(func() {
+ x := 0
+ _ = x
+ f := func() {
+ x = 1
+ }
+ time.AfterFunc(1*time.Second, f)
+ time.AfterFunc(2*time.Second, f)
+ time.Sleep(5 * time.Second)
+ })
+}
diff --git a/src/runtime/synctest.go b/src/runtime/synctest.go
index 65bb15d..f2ac6ab 100644
--- a/src/runtime/synctest.go
+++ b/src/runtime/synctest.go
@@ -79,6 +79,8 @@
} else {
sg.running--
if raceenabled && newval != _Gdead {
+ // Record that this goroutine parking happens before
+ // any subsequent Wait.
racereleasemergeg(gp, sg.raceaddr())
}
}
@@ -133,6 +135,11 @@
// Two wakes happening at the same time leads to very confusing failure modes,
// so we take steps to avoid it happening.
sg.active++
+ next := sg.timers.wakeTime()
+ if next > 0 && next <= sg.now {
+ // A timer is scheduled to fire. Wake the root goroutine to handle it.
+ return sg.root
+ }
if gp := sg.waiter; gp != nil {
// A goroutine is blocked in Wait. Wake it.
return gp
@@ -181,14 +188,14 @@
lock(&sg.mu)
sg.active++
for {
- if raceenabled {
- // Establish a happens-before relationship between a timer being created,
- // and the timer running.
- raceacquireg(gp, gp.syncGroup.raceaddr())
- }
unlock(&sg.mu)
systemstack(func() {
+ // Clear gp.m.curg while running timers,
+ // so timer goroutines inherit their child race context from g0.
+ curg := gp.m.curg
+ gp.m.curg = nil
gp.syncGroup.timers.check(gp.syncGroup.now)
+ gp.m.curg = curg
})
gopark(synctestidle_c, nil, waitReasonSynctestRun, traceBlockSynctest, 0)
lock(&sg.mu)
diff --git a/src/runtime/time.go b/src/runtime/time.go
index 3ece161..d27503e 100644
--- a/src/runtime/time.go
+++ b/src/runtime/time.go
@@ -1080,7 +1080,13 @@
// Note that we are running on a system stack,
// so there is no chance of getg().m being reassigned
// out from under us while this function executes.
- tsLocal := &getg().m.p.ptr().timers
+ gp := getg()
+ var tsLocal *timers
+ if t.ts == nil || t.ts.syncGroup == nil {
+ tsLocal = &gp.m.p.ptr().timers
+ } else {
+ tsLocal = &t.ts.syncGroup.timers
+ }
if tsLocal.raceCtx == 0 {
tsLocal.raceCtx = racegostart(abi.FuncPCABIInternal((*timers).run) + sys.PCQuantum)
}
@@ -1132,7 +1138,11 @@
if gp.racectx != 0 {
throw("unexpected racectx")
}
- gp.racectx = gp.m.p.ptr().timers.raceCtx
+ if ts == nil || ts.syncGroup == nil {
+ gp.racectx = gp.m.p.ptr().timers.raceCtx
+ } else {
+ gp.racectx = ts.syncGroup.timers.raceCtx
+ }
}
if ts != nil {
@@ -1193,6 +1203,11 @@
if ts != nil && ts.syncGroup != nil {
gp := getg()
ts.syncGroup.changegstatus(gp, _Grunning, _Gdead)
+ if raceenabled {
+ // Establish a happens-before between this timer event and
+ // the next synctest.Wait call.
+ racereleasemergeg(gp, ts.syncGroup.raceaddr())
+ }
gp.syncGroup = nil
}