runtime: deliver os/signal notifications under the threads scheduler - #5631
Open
yohimik wants to merge 1 commit into
Open
runtime: deliver os/signal notifications under the threads scheduler#5631yohimik wants to merge 1 commit into
yohimik wants to merge 1 commit into
Conversation
This was referenced Aug 30, 2026
signal.Notify delivers a signal on hosted linux and macOS only while some other goroutine is in time.Sleep. Both hosts default to -scheduler=threads. A program that installs a handler and then waits on the channel, which is what a command does to stop on SIGINT, waits for ever. The receiving goroutine in os/signal calls signal_recv, which parks itself with task.Pause when nothing is pending. Only checkSignals resumes it, and on the receive path the callers of checkSignals are waitForEvents, the idle hook of the cooperative scheduler, and sleepTicks. With threads there is no scheduler loop, so waitForEvents never runs and only a sleep elsewhere in the program lets a signal through. Give the receiver a wait that works on a thread. signal_recv now blocks on a futex of its own that the handler wakes, with the same 0/1 protocol that the handler already uses on signalFutex. It cannot share signalFutex, because sleepTicks waits on that one too and swaps it back to zero, so a time.Sleep anywhere in the program would take the wakeup of the receiver. The handler only gets an atomic store and a futex wake syscall, which are both safe in a signal handler on an arbitrary thread. The stop-the-world signal of the GC uses a different handler that this does not touch. signalWaitUntilIdle, which signal.Stop and signal.Reset call before they return, had the same problem from the other side. It spun on Gosched, which is a no-op with threads, so it used a core until the receiver emptied the last signal. It now waits on a futex that signal_recv wakes. The cooperative path keeps its behaviour. The two versions live in signal_cooperative.go and signal_threads.go, split on scheduler.threads like the schedulers themselves. testdata/signal.go grows a first phase that waits on the channel and nothing else. Built from the current dev branch on macOS 26.6 arm64 it prints nothing and hangs. With this change it prints the expected output and exits.
yohimik
force-pushed
the
upstream-pr/signal-threads
branch
from
September 2, 2026 08:50
545f911 to
369c493
Compare
Author
|
Rebased on dev after the 0.42.0 release. The problem is present in v0.42.0 as |
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.
runtime: deliver os/signal notifications under the threads scheduler
What this does
signal.Notifydelivers a signal on hosted linux and macOS only while someother goroutine is in
time.Sleep. Both hosts default to-scheduler=threads.A program that installs a handler and then waits on the channel, which is what a
command does to stop on SIGINT, waits for ever.
The receiving goroutine in
os/signalcallssignal_recv, which parks itselfwith
task.Pausewhen nothing is pending. OnlycheckSignalsresumes it, andon the receive path the callers of
checkSignalsarewaitForEvents, the idlehook of the cooperative scheduler, and
sleepTicks. With threads there is noscheduler loop, so
waitForEventsnever runs and only a sleep elsewhere in theprogram lets a signal through. This is why the current
testdata/signal.gopasses. It sleeps for 100 ms in
main.signal_recvnow blocks on a futex of its own that the handler wakes, with thesame 0/1 protocol that the handler already uses on
signalFutex. It cannotshare
signalFutex, becausesleepTickswaits on that one too and swaps itback to zero, so a
time.Sleepanywhere in the program would take the wakeup ofthe receiver. The handler only gets an atomic store and a futex wake syscall,
which are both safe in a signal handler on an arbitrary thread. The
stop-the-world signal of the GC uses a different handler that this does not
touch.
signalWaitUntilIdle, whichsignal.Stopandsignal.Resetcall before theyreturn, had the same problem from the other side. It spun on
Gosched, which isa no-op with threads, so it used a core until the receiver emptied the last
signal. It now waits on a futex that
signal_recvwakes.The two versions live in
src/runtime/signal_cooperative.goandsrc/runtime/signal_threads.go, split onscheduler.threadslike theschedulers themselves.
Evidence
testdata/signal.gogets a first phase that waits on the channel and nothingelse, so no timer and no sleep can carry the delivery. It is already in the
TestBuildhost list, so it runs in the linux and the macOS CI jobs with notest-list change.
Built on macOS 26.6 arm64, default scheduler.
got expected signal/got expected signal/exiting signal program, exit 0A downstream product also ships binaries built with a fork that carries this
change, in the published release dispat v1.4.0.
https://github.com/yohimik/dispat/releases/tag/services%2Fdispat%2Fv1.4.0
Scope and known gaps
-scheduler=tasksfails to link on darwin withduplicate symbol: _tinygo_task_exit, and-scheduler=asyncifyfails withundefined: task.SystemStack. Both failures also happen on the current dev branch withtestdata/print.go, so they are separate and are not addressed here.os/signal.signal_ignoredis still missing, sotinygo test os/signaldoesnot link. That is why the test is a
testdataprogram and not the standardlibrary suite. It can be a follow-up.
Related
This is what a command line program needs for an orderly stop on SIGINT or
SIGTERM. It is one of the pieces that made a real CLI work under TinyGo on
hosted linux and macOS.
Related pull requests
This change is part of one body of work. Together the changes make programs that use the network and child processes work on hosted linux and macOS. A full CLI was tested end to end with all of them and ships binaries built this way, see dispat v1.4.0 in the evidence section.
In this repository
In tinygo-org/net
A merge order that works. The three bug fixes are independent. #5633 goes before #5635. HTTPS on linux needs only #5633 and #5635. Full darwin support also needs #5636, the net changes and a new src/net submodule pin.