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/2.
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 wakeup 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. (57)
Please note that not-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 is gnat
.
<thread 1> thread_get_message(a(A)), <thread 2> thread_send_message(b(gnu)), thread_send_message(a(gnat)), |
See also thread_peek_message/1.
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 given Id and number of workers. 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. % work(+Id, +Goal) % % Post work to be done by the pool work(Id, Goal) :- thread_send_message(Id, Goal). |
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 multi-threaded environments should be handled with care as the receiving thread may hold a mutex (see with_mutex). 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 do handle exceptions at any point.
Signals (interrupts) do not cooperate well with the world of multi-threading, 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 of the thread is waiting for (user-)input.
Besides queues (section 6.3.1) threads can share and exchange data using dynamic predicates. The multi-threaded 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 primitive from section 6.4 should be used to ensure 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 as 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 apropriate in the sense of the proper mechanism to reach the goal is still debated. If you have strong feeling in favour or against, please share them in the SWI-Prolog mailing list.