Prolog threads can exchange data using dynamic predicates, database records, and other globally shared data. These provide no suitable means to wait for data or a condition as they can only be checked in an expensive polling loop. Message queues provide a means for threads to wait for data or conditions without using the CPU.
Each thread has a message queue attached to it that is identified by the thread. Additional queues are created using message_queue_create/1. Explicitly created queues come in two flavours. When given an alias, they must be destroyed by the user. Anonymous message queues are identified by a blob (see section 12.4.8) and subject to garbage collection.
If more than one thread is waiting for messages on the given queue and at least one of these is waiting with a partially instantiated Term, the waiting threads are all sent a wake-up signal, starting a rush for the available messages in the queue. This behaviour can seriously harm performance with many threads waiting on the same queue as all-but-the-winner perform a useless scan of the queue. If there is only one waiting thread or all waiting threads wait with an unbound variable, an arbitrary thread is restarted to scan the queue.189See the documentation for the POSIX thread functions pthread_cond_signal() v.s. pthread_cond_broadcast() for background information.
deadline
option. If both options are provided, the earlier time is effective.
If Time is 0 or 0.0, thread_send_message/3 examines the queue and sends the message if space is available, but does not suspend if no space is available, failing immediately instead.
If Time < 0, thread_send_message/3 fails immediately without sending the message.
Please note that non-unifying messages remain in the queue. After the
following has been executed, thread 1 has the term b(gnu)
in its queue and continues execution using A = gnat
.
<thread 1> thread_get_message(a(A)), <thread 2> thread_send_message(Thread_1, b(gnu)), thread_send_message(Thread_1, a(gnat)),
Term may contain attributed variables (see section 8), in which case only terms for which the constraints successfully execute are returned. Handle constraints applies for all predicates that extract terms from message queues. For example, we can get the even numbers from a queue using this code:
get_matching_messages(Queue, Pattern, [H|T]) :- copy_term(Pattern, H), thread_get_message(Queue, H, [timeout(0)]), !, get_matching_messages(Queue, Pattern, T). get_matching_messages(_, _, []). even_numbers(Q, List) :- freeze(Even, Even mod 2 =:= 0), get_matching_messages(Q, Even, List).
See also thread_peek_message/1.
message_queue_create(Queue,[])
. For
compatibility, calling message_queue_create(+Atom)
is
equivalent to
message_queue_create(Queue, [alias(Atom)])
. New code should
use
message_queue_create/2
to create a named queue.deadline
option. If both options are provided, the earlier time is effective.
If Time is 0 or 0.0, thread_get_message/3 examines the queue but does not suspend if no matching term is available. Note that unlike thread_peek_message/2, a matching term is removed from the queue.
If Time < 0, thread_get_message/3 fails immediately without removing any message from the queue.
The size(Size)
property is always present and may be
used to enumerate the created message queues. Note that this predicate
does
not enumerate threads, but can be used to query the properties
of the default queue of a thread.
Explicit message queues are designed with the worker-pool model in mind, where multiple threads wait on a single queue and pick up the first goal to execute. Below is a simple implementation where the workers execute arbitrary Prolog goals. Note that this example provides no means to tell when all work is done. This must be realised using additional synchronisation.
%% create_workers(?Id, +N) % % Create a pool with Id and number of workers. % After the pool is created, post_job/1 can be used to % send jobs to the pool. create_workers(Id, N) :- message_queue_create(Id), forall(between(1, N, _), thread_create(do_work(Id), _, [])). do_work(Id) :- repeat, thread_get_message(Id, Goal), ( catch(Goal, E, print_message(error, E)) -> true ; print_message(error, goal_failed(Goal, worker(Id))) ), fail. %% post_job(+Id, +Goal) % % Post a job to be executed by one of the pool's workers. post_job(Id, Goal) :- thread_send_message(Id, Goal).
While message queues realizes communicating agents sharing the same program and optionally dynamic data, the predicate thread_wait/2 facilitates agents that communicate based on a shared blackboard. An important difference is were message queues require the sender and receiver to know about the queue used to communicate and every message can wakeup at most one thread, the blackboard model allows any number (including zero) of threads to listen to changes on the blackboard. Any module can act as a blackboard. The blackboard can be updated using the standard Prolog database update predicates (assert/1, retract/1 and friends).
Waiting is implemented using a POSIX condition variable and matching mutex. On a matching database change the condition variable is signalled using a broadcast, waking up all threads waiting in thread_wait/2. Multiple database updates can be grouped and cause a single wakeup using thread_update/2. This predicate also allows signalling the module condition variable without updating the database and controlling whether all or a single thread is activated.
The blackboard architecture is a good match for an intelligent agent system that has to react on a changing world. Input threads gather sensor data from the world and update a shared world view in a set of dynamic predicates in one or more modules. Agent threads listen to this data or a subset thereof and trigger actions. This is notably a good match with tabling, in particular incremental tabling (see section 7.7) and Well Founded Semantics (see section 7.6).193Future versions may provide additional triggers, for example to learn about invalidated tables. Please share your experience.
The wait is associated with a module. This module is derived from the Options argument.
The Options list specifies when Goal is re-evaluated and optionally when the call terminates due to a timeout.
wait_preds(+Preds)
is not provided.+
(PI)
195Note
that +p/1
is read as /(+(p),1)., Goal is only triggered if
a clause was added (assert/1).
If the element is -
(PI)
, Goal is
only triggered if a clause was retracted (retract/1
or
erase/1).
Default is to wakeup on both assert and retract.wait_preds
option that have been modified.
List must be unbound at entry.The execution of Goal is synchronized between all threads calling this predicate on the same module, changes to dynamic predicates in this module and calls to thread_update/2 on the same module.
This predicate raises a permision_error
exception when
called recursively or called from inside a transaction. See
section 4.14.1.2 for
details about interaction with transactions.
broadcast
,
default) or a single thread (signal
).
Compatibility The thread_wait/2
predicate is modelled after the
Qu-Prolog
predicate thread_wait_on_goal/2.
It is largely compatible. Our current implementation does not support
predicate time stamps.196See predicate_property/2,
property generation
. We made this predicate
act on a specific module rather than the entire database. The timeout
specification follows that of the other thread waiting predicates and
may be combined with the retry_every
option. The default
retry-time is also 1 second rather than infinite.
These predicates provide a mechanism to make another thread execute some goal as an interrupt. Signalling threads is safe as these interrupts are only checked at safe points in the virtual machine. Nevertheless, signalling in multithreaded environments should be handled with care as the receiving thread may hold a mutex (see with_mutex/2). Signalling probably only makes sense to start debugging threads and to cancel no-longer-needed threads with throw/1, where the receiving thread should be designed carefully to handle exceptions at any point.
Signals (interrupts) do not cooperate well with the world of multithreading, mainly because the status of mutexes cannot be guaranteed easily. At the call port, the Prolog virtual machine holds no locks and therefore the asynchronous execution is safe.
Goal can be any valid Prolog goal, including throw/1 to make the receiving thread generate an exception, and trace/0 to start tracing the receiving thread.
In the Windows version, the receiving thread immediately executes the signal if it reaches a Windows GetMessage() call, which generally happens if the thread is waiting for (user) input.
Besides queues (section 10.3.1) threads can share and exchange data using dynamic predicates. The multithreaded version knows about two types of dynamic predicates. By default, a predicate declared dynamic (see dynamic/1) is shared by all threads. Each thread may assert, retract and run the dynamic predicate. Synchronisation inside Prolog guarantees the consistency of the predicate. Updates are logical: visible clauses are not affected by assert/retract after a query started on the predicate. In many cases primitives from section 10.4 should be used to ensure that application invariants on the predicate are maintained.
Besides shared predicates, dynamic predicates can be declared with the thread_local/1 directive. Such predicates share their attributes, but the clause list is different in each thread.
Thread-local dynamic predicates are intended for maintaining thread-specific state or intermediate results of a computation.
It is not recommended to put clauses for a thread-local predicate into a file, as in the example below, because the clause is only visible from the thread that loaded the source file. All other threads start with an empty clause list.
:- thread_local foo/1. foo(gnat).
DISCLAIMER Whether or not this declaration is appropriate in the sense of the proper mechanism to reach the goal is still debated. If you have strong feelings in favour or against, please share them in the SWI-Prolog mailing list.