internal/task (threads): save stack bounds instead of scanning under a lock

In order to scan stacks, the GC preempts all other threads and has them scan their own stack.
This is somewhat expensive since all of these threads have to fight over a single lock.
Instead, save the stack bounds and let the GC thread perform the scan.

This also fixes a few other bugs I ran into:
1. The GC starts scanning before the world stops. This can cause it to miss some objects (and mistakenly free them) if memory is modified while stopping.
2. The GC does not wait for threads to resume. This can cause notifications to be misinterpreted due to signal nesting if the GC is re-run before all threads wake.
This commit is contained in:
Nia Waldvogel
2025-12-03 15:49:12 -05:00
committed by Ron Evans
parent 20e22d4507
commit 9404bb8712
2 changed files with 104 additions and 90 deletions
+2 -24
View File
@@ -31,10 +31,6 @@ var zeroSizedAlloc uint8
var gcLock task.PMutex
// Normally false, set to true during a GC scan when all other threads get
// paused.
var needsResumeWorld bool
func initHeap() {
libgc_init()
@@ -48,20 +44,8 @@ func gcInit()
//export tinygo_runtime_bdwgc_callback
func gcCallback() {
if hasParallelism && needsResumeWorld {
// Should never happen, check for it anyway.
runtimePanic("gc: world already stopped")
}
// Mark globals and all stacks, and stop the world if we're using threading.
gcMarkReachable()
// If we use a scheduler with parallelism (the threads scheduler for
// example), we need to call gcResumeWorld() after scanning has finished.
if hasParallelism {
// Note that we need to resume the world after finishing the GC call.
needsResumeWorld = true
}
}
func markRoots(start, end uintptr) {
@@ -87,7 +71,6 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
}
gcLock.Lock()
needsResumeWorld = false
var ptr unsafe.Pointer
if layout == gclayout.NoPtrs.AsPtr() {
// This object is entirely pointer free, for example make([]int, ...).
@@ -104,9 +87,7 @@ func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer {
// Memory returned from libgc_malloc has already been zeroed, so nothing
// to do here.
}
if needsResumeWorld {
gcResumeWorld()
}
gcResumeWorld()
gcLock.Unlock()
if ptr == nil {
runtimePanic("gc: out of memory")
@@ -121,11 +102,8 @@ func free(ptr unsafe.Pointer) {
func GC() {
gcLock.Lock()
needsResumeWorld = false
libgc_gcollect()
if needsResumeWorld {
gcResumeWorld()
}
gcResumeWorld()
gcLock.Unlock()
}