runtime: deliver signals under the threads scheduler when blocked on I/O#5530
Open
0pcom wants to merge 1 commit into
Open
runtime: deliver signals under the threads scheduler when blocked on I/O#55300pcom wants to merge 1 commit into
0pcom wants to merge 1 commit into
Conversation
…e sleeps Under the threads scheduler there is no cooperative idle loop, so checkSignals() — which resumes the parked os/signal signal_recv goroutine — was only ever reached from sleepTicks(). That means a signal (e.g. SIGINT/Ctrl+C) was only noticed while some goroutine happened to be inside time.Sleep. A program blocked purely on I/O, channels, mutexes or timers (time.NewTicker uses the timer queue, not sleepTicks) would never observe the signal at all. Start a dedicated signal-watcher thread the first time a signal is enabled, gated to the threads scheduler (!hasScheduler && hasParallelism). It blocks on signalFutex and calls checkSignals() on wake, mirroring the signal half of waitForEvents() that the cooperative scheduler runs from its idle loop. Other schedulers are unaffected (the start is a compile-time no-op for them). Verified: a channel/Accept-blocked program with no time.Sleep now receives SIGINT, and the skycoin daemon (previously unkillable with Ctrl+C under TinyGo) now shuts down cleanly on SIGINT, both idle and during active block sync. (cherry picked from commit ada7ab6)
deadprogram
reviewed
Jul 16, 2026
| // signalFutex and resumes the signal-receiving goroutine (signal_recv) whenever | ||
| // a signal arrives, decoupling signal delivery from sleepTicks(). It mirrors the | ||
| // signal half of waitForEvents(), which the threads scheduler never calls. | ||
| func signalWatcher() { |
Member
There was a problem hiding this comment.
This is called as a Go routine with no exit condition. I know that waitForEvents() already has the same issue, but it would be pretty nice to have a way for a cleaner exit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Under the
threadsscheduler (the default on Linux/macOS), a program blocked purely on I/O, channels or mutexes never observes an OS signal. For example, a server that doessignal.Notify(c, os.Interrupt); <-cwhile its goroutines are blocked on network I/O ignores Ctrl+C indefinitely and has to be killed.The cause:
checkSignals()— which resumes the parkedos/signalsignal_recvgoroutine — is only ever reached fromsleepTicks()(inruntime_unix.go). So a signal is only noticed while some goroutine happens to be insidetime.Sleep.time.NewTicker/time.Aftergo through the timer queue (timerRunner), notsleepTicks, so a program that blocks on I/O/channels can ignore SIGINT indefinitely.The cooperative and multicore schedulers don't have this problem because they call
checkSignals()from their idle loop (waitForEvents). The threads scheduler has no such loop, so nothing consumessignalFutexand resumessignal_recv.Fix
Start a dedicated signal-watcher thread the first time a signal is enabled, gated to the threads scheduler (
!hasScheduler && hasParallelism, which is true only there). It blocks onsignalFutexand callscheckSignals()on wake — mirroring the signal half ofwaitForEvents(). It is a compile-time no-op for every other scheduler: the cooperative/cores schedulers already handle signals from their idle loop, and thenonescheduler has no goroutines.Testing
A channel/
Accept-blocked program with notime.Sleepanywhere ignores SIGINT before this change and exits cleanly after it. Also verified against a real network daemon that blocks on I/O: SIGINT now triggers graceful shutdown, both idle and under load.