During a proprietary project we had the need for an enriched interface for thread_signal/2 and documented the behavior of this interface more precisely. The issue that triggered this was that signals got lost if they were queued (because they could not be processed in time) and a signal earlier raised an exception. Behavior should now be consistent, regardless of whether signals are queued or not. Being able to run goals that cannot be interrupted by signals is now part of the public API (sig_atomic/1) and calls have been added to (un)block signals, inspect and modify the signal queue.
This is on swipl-devel git repo.
If you see flaws or unconventional issues with this interface, please share.
Below are the docs (copy/paste destroyed the layout a little),
The predicates in this section provide signalling between threads. A thread signal inserts any goal as an interrupt into the control flow of any target thread. The target thread processes the goal at the first safe opportunity. The mechanism was introduced with two goals in mind: (1) running a goal inside a thread for debugging purposes such as enabling the status or get access thread-specific data and (2) force a thread to abort its current goal by inserting an exception into its control flow.
Over time, more complicated use cases have been identified that may result in multiple signals that occur (nearly) simultaneous. As of version 8.5.1 the interface has been extended and the interaction with other built-in predicates has been specified in much more detail.
[det] thread_signal (+ThreadId, :Goal)
Make thread ThreadId execute Goal at the first opportunity. The predicate thread_signal/2 itself places Goal into the signalled thread’s signal queue and returns immediately.
ThreadId executes Goal as an interrupt at the first opportunity. Defined opportunities are:
- At the call port of any predicate except for predicates with the property
sig_atomic. Currently this only applies to sig_atomic/1.
- Before retrying a foreign predicate.
- Before backtracking to the next clause of a Prolog predicate.
- When a foreign predicate calls PL_handle_signals(). Foreign predicates that take long to complete should call PL_handle_signals() regularly and return with
FALSEafter PL_handle_signals() returned -1, indicating an exception was raised.
- Foreign predicates calling blocking system calls should attempt to make these system calls interruptible. To enable this on POSIX systems, SWI-Prolog sends a
SIGUSR2to the signalled thread while the handler is an empty function. This causes most blocking system calls to return with
EINTR. See also the commandline option –sig-alert . On Windows, PL_handle_signals() is called when the user processes Windows messages.
- For some blocking (thread) APIs we use a timed version with a 0.25 sec timeout to achieve a polling loop .
If one or more signals are queued, the queue is processed. Processing the queue skips signals blocked due to sig_block/1 and stops after the queue does not contain any more non-blocked signals or processing a signal results in an exception. After an exception, other signals remain in the queue and will be processed after unwinding to the matching catch/3. Typically these queued signals will be processed during the Recover goal of the catch/3. Note that sig_atomic/1 may be used to protect the recovery goal.
The thread_signal/2 mechanism is primarily used by the system to insert debugging goals into the target thread (tspy/1, tbacktrace/1, etc.) or to interrupt a thread using e.g.,
thread_signal(Thread, abort) . Predicates from library
library(thread) use signals to stop workers for e.g. concurrent_maplist/2 if some call fails. Applications may use it, typically for similar purposes such as asynchronously stopping tasks or inspecting the status of a task. Below we describe the behaviour of thread signalling in more detail. The following notes apply for Goal executing in ThreadId
- The execution is protected by sig_atomic/1 and thus signal execution is not nested .
- If Goal succeeds , possible choice points are discarded. Changes to the Prolog stacks such as changes to backtrackable global variables remain.
- If Goal fails , no action is taken, i.e., failure is not considered a special condition.
- If Goal raises an exception the exception is propagated into the environment. This allows for forcefully stopping the target thread. The system uses this to implement abort/0 and call_with_time_limit/2.
- Code into which signals may be injected must make sure to use setup_call_cleanup/3 and friends to ensure proper cleanup in the case of an exception. This is good practice anyway to guard against unpredicatable exceptions such as resource exhaustion.
- Goal may use stack inspection such as prolog_frame_attribute/3 to determine what the thread is doing.
[det] sig_remove (:Pattern, -List)
Remove all signals that unify with Pattern from the signal queue and make the removed signals available in List
[det] sig_block (:Pattern)
Block thread signals queued using thread_signal/2 that match Pattern.
[det] sig_unblock (:Pattern)
Remove any effect of sig_block/1 for patterns that are more specific (see subsumes_term/2). If any patterns are removed, reschedule blocked signals. Note that sig_unblock/1 normally causes all unblocked signals to be executed immediately.
[semidet] sig_atomic (:Goal)
- The goal injected using thread_signal/2, i.e., signals do not interrupt a running signal handler.
- The Setup call of setup_call_cleanup/3 and friends.
- The Cleanup call of call_cleanup/2 and friends.
- Compiling a file or loading a quick load file .
The call port of sig_atomic/1 does not handle signals. This may notably be used to prevent interruption of the catch/3 Recover goal. For example, we may ensure the recovery goal of a timeout is called using the code below. Without this precaution another signal may run before writeln/1 and raise an exception to prevent its execution. Note that catch/3 should generally not be used for cleanup of resources in case of an exception and thus it is typically fine if its Recover goal is interrupted. Use setup_call_cleanup/3 or one of the other predicates from the call_cleanup/2 family for cleanup.
…, catch(call_with_time_limit(Time, Goal), time_limit_exceeded, sig_atomic(writeln(‘Time limit exceeded’))).