10.3.3 Signalling threads
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 exeption 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
FALSE
after 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
SIGUSR2
to the signalled thread while the handler is an empty function. This causes most blocking system calls to return withEINTR
. 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 librarylibrary(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 exeception 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.
- At the call port of any predicate except for predicates
with the property
- [det]sig_pending(-List)
- True when List contains all signals submitted using thread_signal/2 that are not yet processed. This includes signals blocked by sig_block/1.
- [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)
- Execute Goal as once/1
while blocking both thread signals (see thread_signal/2)
and OS signals (see on_signal/3).
The system executes some goals while blocking signals. These are:
- 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'))).