[Git][ghc/ghc][master] 5 commits: Adjust releaseCapability_ precondition to allow cap->running_task == NULL
Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC Commits: 6e381626 by Duncan Coutts at 2026-07-01T22:29:55+01:00 Adjust releaseCapability_ precondition to allow cap->running_task == NULL There are two use cases for releaseCapability_: 1. The current Task (cap->running_task) releases the Capability. The Capability is marked free, and if there is any work to do, an appropriate Task is woken up. 2. There is no current task (cap->task == NULL), and thus the Capability is idle, and we want to wake up an idle Task to animate the Capability. This case uses always_wakeup. Currently, the precondition for releaseCapability_ is cap->running_task != NULL and so the 2nd use cases have to set cap->running_task (which is then immediately overwritten) just to satisfy the precondition. See the use cases in sendMessage and prodCapability. So we can relax the precondition to be: cap->running_task != NULL || always_wakeup so that in the always_wakeup case, we say it is ok for the cap->running_task to be NULL. This lets us simplify sendMessage and prodCapability. In particular it will allow prodCapability to not need a Task parameter. The ulterior motive for all this is that I want to be able to call prodCapability from an OS thread that is not itself a Task, in persuit of issue #27086: disentangle I/O managers from wakeUpRts. The most straightforward way to wake the RTS is using prodCapability, but the context in which we will need to do that are threads that are not Tasks. - - - - - 89404ebc by Duncan Coutts at 2026-07-01T22:29:55+01:00 prodCapability no longer needs to take a Task param Now that releaseCapability_ can accept cap->running_task == NULL then it is no longer necessary for prodCapability to require a Task. - - - - - 4e60c5f6 by Duncan Coutts at 2026-07-01T22:29:56+01:00 Define prodOneCapability There was an existing declaration for this in the header file, but no definition. Similarly, there is a declaration for prodAllCapabilities but no definition, and we don't need it, so remove the declaration. - - - - - 2527026f by Duncan Coutts at 2026-07-01T22:29:56+01:00 Add a wakeUpRtsViaTicker feature to the posix ticker It proxies a call to wakeUpRts, but crucially, this can be called from a signal handler context. It will be used for ctl-c handling. - - - - - aa5a03a5 by Duncan Coutts at 2026-07-01T22:29:56+01:00 Change how wakeUpRts works Previously it would call wakeupIOManager to get a capability to wake up and run. This works but it entangles the I/O managers with unrelated features: ctl-c handling and idle gc (the two features that use wakeUpRts). The reason it used wakeupIOManager is that this action is safe to use from a posix signal handler, since it just posts bytes to a pipe. Otherwise the more direct approach (used e.g. by sendMessage when the target capability is idle) is to use releaseCapability. But that uses condition variables and mutexes, which are not safe to use from within a signal handler. So instead of entangling the (multiple) I/O managers with this, we make wakeUpRts use the direct approach (using prodOneCapability). On win32 the ctl-c console handler can call wakeUpRts directly, since it is called in a proper thread. On posix, to deal with the signal handler problem, we make the signal handler ask the ticker thread to proxy the call to wakeUpRts, since the ticker thread is also a proper thread. This will allow the I/O managers to no longer be concerned with this. This is good because there are many I/O managers (and they're complicated), but there is (on posix) only one ticker implementation. So this is an overall reduction in coupling and complexity. Fixes issue #27086 - - - - - 7 changed files: - rts/Capability.c - rts/Capability.h - rts/Messages.c - rts/Schedule.c - rts/Ticker.h - rts/posix/Ticker.c - rts/sm/GC.c Changes: ===================================== rts/Capability.c ===================================== @@ -525,13 +525,20 @@ giveCapabilityToTask (Capability *cap USED_IF_DEBUG, Task *task) /* ---------------------------------------------------------------------------- * releaseCapability * - * The current Task (cap->task) releases the Capability. The Capability is - * marked free, and if there is any work to do, an appropriate Task is woken up. + * This serves two purposes: + * + * 1. The current Task (cap->running_task) releases the Capability. + * The Capability is marked free, and if there is any work to do, an + * appropriate Task is woken up. + * + * 2. There is no current task (cap->task == NULL), and thus the Capability + * is idle, and we want to wake up an idle Task to animate the Capability. + * In this case set always_wakeup. See also prodCapability. * * The caller must hold cap->lock and will still hold it after * releaseCapability returns. * - * N.B. May need to take all_tasks_mutex. + * N.B. May need to take all_tasks_mutex, if it needs to start a new task. * * ------------------------------------------------------------------------- */ @@ -540,12 +547,18 @@ void releaseCapability_ (Capability* cap, bool always_wakeup) { - Task *task; - - task = cap->running_task; - - ASSERT_PARTIAL_CAPABILITY_INVARIANTS(cap,task); - ASSERT_RETURNING_TASKS(cap,task); + { + Task *task = cap->running_task; + + ASSERT(task || always_wakeup); + // To cover purpose 2 above, we allow the cap->running_task to be + // NULL, to handle cases where a thread (that is not itself a Task) + // needs to wake up an idle task for the capability. + if (task) { + ASSERT_PARTIAL_CAPABILITY_INVARIANTS(cap,task); + ASSERT_RETURNING_TASKS(cap,task); + } + } ASSERT_LOCK_HELD(&cap->lock); RELAXED_STORE(&cap->running_task, NULL); @@ -581,8 +594,8 @@ releaseCapability_ (Capability* cap, // assertion is false: in schedule() we force a yield after // ThreadBlocked, but the thread may be back on the run queue // by now. - task = peekRunQueue(cap)->bound->task; - giveCapabilityToTask(cap, task); + Task *btask = peekRunQueue(cap)->bound->task; + giveCapabilityToTask(cap, btask); return; } @@ -1087,16 +1100,25 @@ yieldCapability #if defined(THREADED_RTS) void -prodCapability (Capability *cap, Task *task) +prodCapability (Capability *cap) { ACQUIRE_LOCK(&cap->lock); if (!cap->running_task) { - cap->running_task = task; releaseCapability_(cap,true); } RELEASE_LOCK(&cap->lock); } +/* Ensure at least one capability is not idle. Used to wake up the RTS + * in cases where we anticipate that all capabilities may be idle. In + * particular it is used for the ctl-c handler, and after the idle GC + * timeout to initiate idle GC. */ +void +prodOneCapability (void) +{ + prodCapability(getCapability(0)); +} + #endif /* THREADED_RTS */ /* ---------------------------------------------------------------------------- ===================================== rts/Capability.h ===================================== @@ -348,11 +348,7 @@ bool yieldCapability (Capability** pCap, Task *task, bool gcAllowed); // need to service some global event. // void prodOneCapability (void); -void prodCapability (Capability *cap, Task *task); - -// Similar to prodOneCapability(), but prods all of them. -// -void prodAllCapabilities (void); +void prodCapability (Capability *cap); // Attempt to gain control of a Capability if it is free. // ===================================== rts/Messages.c ===================================== @@ -49,9 +49,11 @@ void sendMessage(Capability *from_cap, Capability *to_cap, Message *msg) recordClosureMutated(from_cap,(StgClosure*)msg); if (to_cap->running_task == NULL) { - to_cap->running_task = myTask(); - // precond for releaseCapability_() - releaseCapability_(to_cap,false); + /* Precond for releaseCapability_ is: running_task || always_wakeup. + * We have running_task == NULL, hence we must use always_wakeup. This + * is ok since the inbox is now non-empty, so we wake a task anyway. + */ + releaseCapability_(to_cap, true /*always_wakeup*/); } else { interruptCapability(to_cap); } ===================================== rts/Schedule.c ===================================== @@ -2885,8 +2885,8 @@ performBlockingMajorGC(void) } /* --------------------------------------------------------------------------- - Interrupt execution. - Might be called inside a signal handler so it mustn't do anything fancy. + Interrupt execution in response to ctl-c. + On posix, ctl-c is a signal, while on Win32 it is a console event. ------------------------------------------------------------------------ */ void @@ -2896,17 +2896,37 @@ interruptStgRts(void) setSchedState(SCHED_INTERRUPTING); interruptAllCapabilities(); #if defined(THREADED_RTS) + /* It may be that all capabilities are idle. If so, we must wake one up. */ +#if defined(mingw32_HOST_OS) + /* On win32, console handlers are invoked in a proper thread, so we can + * directly call wakeUpRts. Although it is an OS thread, it is not one + * we created or control necessarily, so it may have no associated Task. + */ wakeUpRts(); +#else + /* On posix on the other hand, signal handlers are very limited in what + * they can do. We cannot directly call wakeUpRts below because it is not + * signal safe (it uses cond vars to wake up a task). So instead we proxy + * it: we interrupt the ticker thread and ask the ticker thread to call + * wakeUpRts below. The ticker thread is a proper thread and so can call + * wakeUpRts. We can interrupt the ticker thread from signal handler + * context safely because it only involves writing to a pipe/eventfd. + */ + wakeUpRtsViaTicker(); +#endif #endif } /* ----------------------------------------------------------------------------- Wake up the RTS - This function causes at least one OS thread to wake up and run the - scheduler loop. It is invoked when the RTS might be deadlocked, or - an external event has arrived that may need servicing (eg. a - keyboard interrupt). + This function causes at least one task to wake up and run the scheduler + loop on at least one capability. + + It is invoked: + 1. as part of the idle GC scheme: when the RTS has been idle for long enough + and it is time to go back to the scheduler which will invoke idle GC; or + 2. when a ctl-c occurs (posix sigint signal or win32 console event) In the single-threaded RTS we don't do anything here; we only have one thread anyway, and the event that caused us to want to wake up @@ -2916,10 +2936,11 @@ interruptStgRts(void) #if defined(THREADED_RTS) void wakeUpRts(void) { - // This forces the IO Manager thread to wakeup, which will - // in turn ensure that some OS thread wakes up and runs the - // scheduler loop, which will cause a GC and deadlock check. - wakeupIOManager(); + /* Our current thread may not have a Task, in particular it will not when + * called from interruptStgRts or via wakeUpRtsViaTicker. This is ok, + * prodOneCapability does not require one. + */ + prodOneCapability(); } #endif ===================================== rts/Ticker.h ===================================== @@ -52,4 +52,8 @@ void exitTicker(void); void pauseTicker(void); void unpauseTicker(void); +#if defined(THREADED_RTS) +void wakeUpRtsViaTicker(void); +#endif + #include "EndPrivate.h" ===================================== rts/posix/Ticker.c ===================================== @@ -127,6 +127,13 @@ static Time ticker_interval = DEFAULT_TICK_INTERVAL; // acknowledgement. static bool pause_request; +#if defined(THREADED_RTS) +// Atomic variable used by the ctl-c handler (posix signal handler) to +// communicate that the ticker thread should wake up the rts. This +// communication is one-way, with no acknowledgement. +static bool interrupt_request; +#endif + // Atomic variable used by other threads to communicate that they want the // ticker thread to exit. static bool exit_request; @@ -177,6 +184,13 @@ static void *ticker_thread_func(void *_handle_tick) paused = ACQUIRE_LOAD_ALWAYS(&pause_request); exit = RELAXED_LOAD_ALWAYS(&exit_request); + +#if defined(THREADED_RTS) + if (RELAXED_LOAD_ALWAYS(&interrupt_request)) { + RELEASE_STORE_ALWAYS(&interrupt_request, false); + wakeUpRts(); + } +#endif } else if (errno != EINTR) { // While the RTS attempts to mask signals, some foreign libraries // that rely on signal delivery may unmask them. Consequently we @@ -262,6 +276,14 @@ void pauseTicker(void) sendFdWakeup(notifyfd_w); } +#if defined(THREADED_RTS) +void wakeUpRtsViaTicker(void) +{ + RELAXED_STORE_ALWAYS(&interrupt_request, true); + sendFdWakeup(notifyfd_w); +} +#endif + /* Synchronous. Not idempotent. * The ticker is guaranteed stopped after this. */ ===================================== rts/sm/GC.c ===================================== @@ -1522,7 +1522,7 @@ waitForGcThreads (Capability *cap, bool idle_cap[]) for(i = 0; i < getNumCapabilities(); ++i) { if (i == me || idle_cap[i]) { continue; } if (SEQ_CST_LOAD(&gc_threads[i]->wakeup) != GC_THREAD_STANDING_BY) { - prodCapability(getCapability(i), cap->running_task); + prodCapability(getCapability(i)); interruptCapability(getCapability(i)); } } View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/8585f8cb561e0ac1a995e1fd45ee521... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/compare/8585f8cb561e0ac1a995e1fd45ee521... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Marge Bot (@marge-bot)