| // Copyright 2024 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 runtime |
| |
| import ( |
| "unsafe" |
| ) |
| |
| // A synctestGroup is a group of goroutines started by synctest.Run. |
| type synctestGroup struct { |
| mu mutex |
| timers timers |
| now int64 // current fake time |
| root *g // caller of synctest.Run |
| waiter *g // caller of synctest.Wait |
| waiting bool // true if a goroutine is calling synctest.Wait |
| |
| // The group is active (not blocked) so long as running > 0 || active > 0. |
| // |
| // running is the number of goroutines which are not "durably blocked": |
| // Goroutines which are either running, runnable, or non-durably blocked |
| // (for example, blocked in a syscall). |
| // |
| // active is used to keep the group from becoming blocked, |
| // even if all goroutines in the group are blocked. |
| // For example, park_m can choose to immediately unpark a goroutine after parking it. |
| // It increments the active count to keep the group active until it has determined |
| // that the park operation has completed. |
| total int // total goroutines |
| running int // non-blocked goroutines |
| active int // other sources of activity |
| } |
| |
| // changegstatus is called when the non-lock status of a g changes. |
| // It is never called with a Gscanstatus. |
| func (sg *synctestGroup) changegstatus(gp *g, oldval, newval uint32) { |
| // Determine whether this change in status affects the idleness of the group. |
| // If this isn't a goroutine starting, stopping, durably blocking, |
| // or waking up after durably blocking, then return immediately without |
| // locking sg.mu. |
| // |
| // For example, stack growth (newstack) will changegstatus |
| // from _Grunning to _Gcopystack. This is uninteresting to synctest, |
| // but if stack growth occurs while sg.mu is held, we must not recursively lock. |
| totalDelta := 0 |
| wasRunning := true |
| switch oldval { |
| case _Gdead: |
| wasRunning = false |
| totalDelta++ |
| case _Gwaiting: |
| if gp.waitreason.isIdleInSynctest() { |
| wasRunning = false |
| } |
| } |
| isRunning := true |
| switch newval { |
| case _Gdead: |
| isRunning = false |
| totalDelta-- |
| case _Gwaiting: |
| if gp.waitreason.isIdleInSynctest() { |
| isRunning = false |
| } |
| } |
| // It's possible for wasRunning == isRunning while totalDelta != 0; |
| // for example, if a new goroutine is created in a non-running state. |
| if wasRunning == isRunning && totalDelta == 0 { |
| return |
| } |
| |
| lock(&sg.mu) |
| sg.total += totalDelta |
| if wasRunning != isRunning { |
| if isRunning { |
| sg.running++ |
| } else { |
| sg.running-- |
| if raceenabled && newval != _Gdead { |
| racereleasemergeg(gp, sg.raceaddr()) |
| } |
| } |
| } |
| if sg.total < 0 { |
| fatal("total < 0") |
| } |
| if sg.running < 0 { |
| fatal("running < 0") |
| } |
| wake := sg.maybeWakeLocked() |
| unlock(&sg.mu) |
| if wake != nil { |
| goready(wake, 0) |
| } |
| } |
| |
| // incActive increments the active-count for the group. |
| // A group does not become durably blocked while the active-count is non-zero. |
| func (sg *synctestGroup) incActive() { |
| lock(&sg.mu) |
| sg.active++ |
| unlock(&sg.mu) |
| } |
| |
| // decActive decrements the active-count for the group. |
| func (sg *synctestGroup) decActive() { |
| lock(&sg.mu) |
| sg.active-- |
| if sg.active < 0 { |
| throw("active < 0") |
| } |
| wake := sg.maybeWakeLocked() |
| unlock(&sg.mu) |
| if wake != nil { |
| goready(wake, 0) |
| } |
| } |
| |
| // maybeWakeLocked returns a g to wake if the group is durably blocked. |
| func (sg *synctestGroup) maybeWakeLocked() *g { |
| if sg.running > 0 || sg.active > 0 { |
| return nil |
| } |
| // Increment the group active count, since we've determined to wake something. |
| // The woken goroutine will decrement the count. |
| // We can't just call goready and let it increment sg.running, |
| // since we can't call goready with sg.mu held. |
| // |
| // Incrementing the active count here is only necessary if something has gone wrong, |
| // and a goroutine that we considered durably blocked wakes up unexpectedly. |
| // Two wakes happening at the same time leads to very confusing failure modes, |
| // so we take steps to avoid it happening. |
| sg.active++ |
| if gp := sg.waiter; gp != nil { |
| // A goroutine is blocked in Wait. Wake it. |
| return gp |
| } |
| // All goroutines in the group are durably blocked, and nothing has called Wait. |
| // Wake the root goroutine. |
| return sg.root |
| } |
| |
| func (sg *synctestGroup) raceaddr() unsafe.Pointer { |
| // Address used to record happens-before relationships created by the group. |
| // |
| // Wait creates a happens-before relationship between itself and |
| // the blocking operations which caused other goroutines in the group to park. |
| return unsafe.Pointer(sg) |
| } |
| |
| //go:linkname synctestRun internal/synctest.Run |
| func synctestRun(f func()) { |
| if debug.asynctimerchan.Load() != 0 { |
| panic("synctest.Run not supported with asynctimerchan!=0") |
| } |
| |
| gp := getg() |
| if gp.syncGroup != nil { |
| panic("synctest.Run called from within a synctest bubble") |
| } |
| gp.syncGroup = &synctestGroup{ |
| total: 1, |
| running: 1, |
| root: gp, |
| } |
| const synctestBaseTime = 946684800000000000 // midnight UTC 2000-01-01 |
| gp.syncGroup.now = synctestBaseTime |
| gp.syncGroup.timers.syncGroup = gp.syncGroup |
| lockInit(&gp.syncGroup.mu, lockRankSynctest) |
| lockInit(&gp.syncGroup.timers.mu, lockRankTimers) |
| defer func() { |
| gp.syncGroup = nil |
| }() |
| |
| fv := *(**funcval)(unsafe.Pointer(&f)) |
| newproc(fv) |
| |
| sg := gp.syncGroup |
| lock(&sg.mu) |
| sg.active++ |
| for { |
| if raceenabled { |
| raceacquireg(gp, gp.syncGroup.raceaddr()) |
| } |
| unlock(&sg.mu) |
| systemstack(func() { |
| gp.syncGroup.timers.check(gp.syncGroup.now) |
| }) |
| gopark(synctestidle_c, nil, waitReasonSynctestRun, traceBlockSynctest, 0) |
| lock(&sg.mu) |
| if sg.active < 0 { |
| throw("active < 0") |
| } |
| next := sg.timers.wakeTime() |
| if next == 0 { |
| break |
| } |
| if next < sg.now { |
| throw("time went backwards") |
| } |
| sg.now = next |
| } |
| |
| total := sg.total |
| unlock(&sg.mu) |
| if total != 1 { |
| panic("deadlock: all goroutines in bubble are blocked") |
| } |
| if gp.timer != nil && gp.timer.isFake { |
| // Verify that we haven't marked this goroutine's sleep timer as fake. |
| // This could happen if something in Run were to call timeSleep. |
| throw("synctest root goroutine has a fake timer") |
| } |
| } |
| |
| func synctestidle_c(gp *g, _ unsafe.Pointer) bool { |
| lock(&gp.syncGroup.mu) |
| canIdle := true |
| if gp.syncGroup.running == 0 && gp.syncGroup.active == 1 { |
| // All goroutines in the group have blocked or exited. |
| canIdle = false |
| } else { |
| gp.syncGroup.active-- |
| } |
| unlock(&gp.syncGroup.mu) |
| return canIdle |
| } |
| |
| //go:linkname synctestWait internal/synctest.Wait |
| func synctestWait() { |
| gp := getg() |
| if gp.syncGroup == nil { |
| panic("goroutine is not in a bubble") |
| } |
| lock(&gp.syncGroup.mu) |
| // We use a syncGroup.waiting bool to detect simultaneous calls to Wait rather than |
| // checking to see if syncGroup.waiter is non-nil. This avoids a race between unlocking |
| // syncGroup.mu and setting syncGroup.waiter while parking. |
| if gp.syncGroup.waiting { |
| unlock(&gp.syncGroup.mu) |
| panic("wait already in progress") |
| } |
| gp.syncGroup.waiting = true |
| unlock(&gp.syncGroup.mu) |
| gopark(synctestwait_c, nil, waitReasonSynctestWait, traceBlockSynctest, 0) |
| |
| lock(&gp.syncGroup.mu) |
| gp.syncGroup.active-- |
| if gp.syncGroup.active < 0 { |
| throw("active < 0") |
| } |
| gp.syncGroup.waiter = nil |
| gp.syncGroup.waiting = false |
| unlock(&gp.syncGroup.mu) |
| |
| // Establish a happens-before relationship on the activity of the now-blocked |
| // goroutines in the group. |
| if raceenabled { |
| raceacquireg(gp, gp.syncGroup.raceaddr()) |
| } |
| } |
| |
| func synctestwait_c(gp *g, _ unsafe.Pointer) bool { |
| lock(&gp.syncGroup.mu) |
| if gp.syncGroup.running == 0 && gp.syncGroup.active == 0 { |
| // This shouldn't be possible, since gopark increments active during unlockf. |
| throw("running == 0 && active == 0") |
| } |
| gp.syncGroup.waiter = gp |
| unlock(&gp.syncGroup.mu) |
| return true |
| } |
| |
| //go:linkname synctest_acquire internal/synctest.acquire |
| func synctest_acquire() any { |
| if sg := getg().syncGroup; sg != nil { |
| sg.incActive() |
| return sg |
| } |
| return nil |
| } |
| |
| //go:linkname synctest_release internal/synctest.release |
| func synctest_release(sg any) { |
| sg.(*synctestGroup).decActive() |
| } |
| |
| //go:linkname synctest_inBubble internal/synctest.inBubble |
| func synctest_inBubble(sg any, f func()) { |
| gp := getg() |
| if gp.syncGroup != nil { |
| panic("goroutine is already bubbled") |
| } |
| gp.syncGroup = sg.(*synctestGroup) |
| defer func() { |
| gp.syncGroup = nil |
| }() |
| f() |
| } |