From cd6f7422fba07d536c92123980c277b98e5f5f96 Mon Sep 17 00:00:00 2001 From: Dave Cheney Date: Tue, 29 Sep 2026 18:49:18 +1000 Subject: [PATCH] runtime: yield the thread in Gosched on the threads scheduler Gosched was a no-op on the threads scheduler. Add task.Yield, which calls sched_yield, and use it from Gosched to ask the OS scheduler to let other threads run. Each goroutine has its own OS thread. This differs from upstream Go's Gosched, which hands off to the Go scheduler through gosched_m. Upstream uses osyield in spin loops, including locking and stop-the-world code, not to implement Gosched. This reduces an iter test flake on Linux. stableNumGoroutine in iter/pull_test.go uses GOMAXPROCS(1) and Gosched to let the previous subtest's tRunner goroutine finish exiting. TinyGo ignores GOMAXPROCS, so sched_yield cannot guarantee that goroutine has exited or rule out the race. The original Linux stress test under full CPU load saw TestPull and TestPull2 fail 7 of 300 runs before this change and 0 of 300 after. Remove the outdated signal_waitUntilIdle TODO. Its polling loop now yields on each pass with the threads scheduler. --- src/internal/task/task_threads.go | 8 ++++++++ src/runtime/runtime_unix.go | 3 --- src/runtime/scheduler_threads.go | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/internal/task/task_threads.go b/src/internal/task/task_threads.go index 40e14df194..c175bedbfc 100644 --- a/src/internal/task/task_threads.go +++ b/src/internal/task/task_threads.go @@ -202,6 +202,11 @@ func CoroExit(next *Task) { tinygo_task_exit() } +// Yield yields the current thread to the OS scheduler. +func Yield() { + sched_yield() +} + // scanWaitGroup is used to wait on until all threads have finished the current state transition. var scanWaitGroup waitGroup @@ -372,6 +377,9 @@ func tinygo_task_send_gc_signal(threadID) //export tinygo_task_current func tinygo_task_current() unsafe.Pointer +//export sched_yield +func sched_yield() int32 + func NumCPU() int { return int(numCPU) } diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go index e1273059b7..14b544b991 100644 --- a/src/runtime/runtime_unix.go +++ b/src/runtime/runtime_unix.go @@ -418,9 +418,6 @@ func signal_disable(s uint32) { func signal_waitUntilIdle() { // Wait until signal_recv has processed all signals. for receivedSignals.Load() != 0 { - // TODO: this becomes a busy loop when using threads. - // We might want to pause until signal_recv has no more incoming signals - // to process. Gosched() } } diff --git a/src/runtime/scheduler_threads.go b/src/runtime/scheduler_threads.go index 90615b08d8..a6c7750d6e 100644 --- a/src/runtime/scheduler_threads.go +++ b/src/runtime/scheduler_threads.go @@ -61,9 +61,9 @@ func scheduleTaskNoWake(t *task.Task) { } func Gosched() { - // Each goroutine runs in a thread, so there's not much we can do here. - // There is sched_yield but it's only really intended for realtime - // operation, so is probably best not to use. + // Each goroutine runs in a thread, so yield the thread to let other + // goroutines make progress. + task.Yield() } // NumCPU returns the number of logical CPUs usable by the current process.