aboutsummaryrefslogtreecommitdiff
path: root/src/runtime/proc.go
diff options
context:
space:
mode:
authorMichael Anthony Knyszek <mknyszek@google.com>2025-12-04 23:27:03 +0000
committerMichael Knyszek <mknyszek@google.com>2025-12-05 13:48:24 -0800
commit745349712e837ef77eb7b5a21c4d4e5c7ca0371a (patch)
treeeaa0623b145ef591251f6a174b2140ef5cfbe170 /src/runtime/proc.go
parentf3d572d96a25d1a0956ef828c0ff510ebf214d22 (diff)
downloadgo-745349712e837ef77eb7b5a21c4d4e5c7ca0371a.tar.xz
runtime: don't count nGsyscallNoP for extra Ms in C
In #76435, it turns out that the new metric /sched/goroutines/not-in-go:goroutines counts C threads that have called into Go before (on Linux) as not-in-go goroutines. The reason for this is that the M is still attached to the C thread on Linux as an optimization, so we don't go through all the trouble of detaching the M and, of course, decrementing nGsyscallNoP. There's an easy fix to this accounting issue. The flag on the M, isExtraInC, says whether a thread with an extra M attached no longer has any Go on its (logical) stack. When we take the P from an M in this state, we simply just don't increment nGsyscallNoP. When it calls back into Go, we similarly skip the decrement to nGsyscallNoP. This is more efficient than alternatives, like always updating nGsyscallNoP in cgocallbackg, since that would add a new read-modify-write atomic onto that fast path. It does mean we count threads in C with a P still attached as not-in-go, but this transient in most real programs, assuming the thread indeed does not call back into Go any time soon. Fixes #76435. Change-Id: Id05563bacbe35d3fae17d67fb5ed45fa43fa0548 Reviewed-on: https://go-review.googlesource.com/c/go/+/726964 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Pratt <mpratt@google.com>
Diffstat (limited to 'src/runtime/proc.go')
-rw-r--r--src/runtime/proc.go54
1 files changed, 45 insertions, 9 deletions
diff --git a/src/runtime/proc.go b/src/runtime/proc.go
index 16538098cf..52def488ff 100644
--- a/src/runtime/proc.go
+++ b/src/runtime/proc.go
@@ -2433,7 +2433,7 @@ func needm(signal bool) {
sp := sys.GetCallerSP()
callbackUpdateSystemStack(mp, sp, signal)
- // Should mark we are already in Go now.
+ // We must mark that we are already in Go now.
// Otherwise, we may call needm again when we get a signal, before cgocallbackg1,
// which means the extram list may be empty, that will cause a deadlock.
mp.isExtraInC = false
@@ -2455,7 +2455,8 @@ func needm(signal bool) {
// mp.curg is now a real goroutine.
casgstatus(mp.curg, _Gdeadextra, _Gsyscall)
sched.ngsys.Add(-1)
- sched.nGsyscallNoP.Add(1)
+ // N.B. We do not update nGsyscallNoP, because isExtraInC threads are not
+ // counted as real goroutines while they're in C.
if !signal {
if trace.ok() {
@@ -2590,7 +2591,7 @@ func dropm() {
casgstatus(mp.curg, _Gsyscall, _Gdeadextra)
mp.curg.preemptStop = false
sched.ngsys.Add(1)
- sched.nGsyscallNoP.Add(-1)
+ decGSyscallNoP(mp)
if !mp.isExtraInSig {
if trace.ok() {
@@ -4732,7 +4733,7 @@ func entersyscallHandleGCWait(trace traceLocker) {
if trace.ok() {
trace.ProcStop(pp)
}
- sched.nGsyscallNoP.Add(1)
+ addGSyscallNoP(gp.m) // We gave up our P voluntarily.
pp.gcStopTime = nanotime()
pp.syscalltick++
if sched.stopwait--; sched.stopwait == 0 {
@@ -4763,7 +4764,7 @@ func entersyscallblock() {
gp.m.syscalltick = gp.m.p.ptr().syscalltick
gp.m.p.ptr().syscalltick++
- sched.nGsyscallNoP.Add(1)
+ addGSyscallNoP(gp.m) // We're going to give up our P.
// Leave SP around for GC and traceback.
pc := sys.GetCallerPC()
@@ -5001,8 +5002,8 @@ func exitsyscallTryGetP(oldp *p) *p {
if oldp != nil {
if thread, ok := setBlockOnExitSyscall(oldp); ok {
thread.takeP()
+ addGSyscallNoP(thread.mp) // takeP does the opposite, but this is a net zero change.
thread.resume()
- sched.nGsyscallNoP.Add(-1) // takeP adds 1.
return oldp
}
}
@@ -5017,7 +5018,7 @@ func exitsyscallTryGetP(oldp *p) *p {
}
unlock(&sched.lock)
if pp != nil {
- sched.nGsyscallNoP.Add(-1)
+ decGSyscallNoP(getg().m) // We got a P for ourselves.
return pp
}
}
@@ -5043,7 +5044,7 @@ func exitsyscallNoP(gp *g) {
trace.GoSysExit(true)
traceRelease(trace)
}
- sched.nGsyscallNoP.Add(-1)
+ decGSyscallNoP(getg().m)
dropg()
lock(&sched.lock)
var pp *p
@@ -5081,6 +5082,41 @@ func exitsyscallNoP(gp *g) {
schedule() // Never returns.
}
+// addGSyscallNoP must be called when a goroutine in a syscall loses its P.
+// This function updates all relevant accounting.
+//
+// nosplit because it's called on the syscall paths.
+//
+//go:nosplit
+func addGSyscallNoP(mp *m) {
+ // It's safe to read isExtraInC here because it's only mutated
+ // outside of _Gsyscall, and we know this thread is attached
+ // to a goroutine in _Gsyscall and blocked from exiting.
+ if !mp.isExtraInC {
+ // Increment nGsyscallNoP since we're taking away a P
+ // from a _Gsyscall goroutine, but only if isExtraInC
+ // is not set on the M. If it is, then this thread is
+ // back to being a full C thread, and will just inflate
+ // the count of not-in-go goroutines. See go.dev/issue/76435.
+ sched.nGsyscallNoP.Add(1)
+ }
+}
+
+// decGSsyscallNoP must be called whenever a goroutine in a syscall without
+// a P exits the system call. This function updates all relevant accounting.
+//
+// nosplit because it's called from dropm.
+//
+//go:nosplit
+func decGSyscallNoP(mp *m) {
+ // Update nGsyscallNoP, but only if this is not a thread coming
+ // out of C. See the comment in addGSyscallNoP. This logic must match,
+ // to avoid unmatched increments and decrements.
+ if !mp.isExtraInC {
+ sched.nGsyscallNoP.Add(-1)
+ }
+}
+
// Called from syscall package before fork.
//
// syscall_runtime_BeforeFork is for package syscall,
@@ -6758,7 +6794,7 @@ func (s syscallingThread) releaseP(state uint32) {
trace.ProcSteal(s.pp)
traceRelease(trace)
}
- sched.nGsyscallNoP.Add(1)
+ addGSyscallNoP(s.mp)
s.pp.syscalltick++
}