Logtalk user manual
Multi-threading programming

Multi-threading programming

Logtalk provides experimental support for multi-threading programming on selected Prolog compilers. Logtalk makes use of the low-level Prolog built-in predicates that interface with POSIX threads (or a suitable emulation), providing a small set of high-level predicates and directives that allows programmers to easily take advantage of modern multi-processor and multi-core computers without caring about the details of creating, synchronizing, or communicating with threads. Logtalk multi-threading programming integrates with object-oriented programming by supporting per object threads, enabling objects to prove goals asynchronously (including call to local predicates and message sending goals). This allows an object to send both synchronous and asynchronous messages.

Enabling multi-threading support

Multi-threading support may be disabled by default. It can be enabled on the Prolog configuration files of supported compilers by setting the read-only compiler flag threads to on.

Object threads

In order to automatically create and set up an object's thread the threaded/0 object directive must be used:

:- threaded.

The thread is created when the object is loaded or created at runtime. The thread for the pseudo-object user is automatically created when Logtalk is loaded (provided that multi-threading programming is supported and enabled for the chosen Prolog compiler).

The message queue associated with an object's thread receives both goals to be proved asynchronously and the replies to the threaded calls made from within the object itself.

Multi-threading built-in predicates

Logtalk provides a small set of built-in predicates for multi-threading programming. For simple tasks where you simply want to prove a set of goals, each one in its own thread, Logtalk provides a threaded/1 built-in predicate. The remaining predicates allow for fine-grained control, including postponing retrieving of thread goal results at a later time, supporting non-deterministic thread goals, and make one-way asynchronous calls. Together, these predicates provide high-level support for multi-threading programming, covering most common use cases.

Proving goals concurrently using threads

A conjunction of goals may be proved concurrently by calling the Logtalk built-in predicate threaded/1. Each goal in the conjunction runs in its own thread. This is akin to and-parallelism. For example, assume that we want to find all the prime numbers in a given interval, [N, M]. We can split the interval in two parts and then span two threads to compute the primes numbers in each sub-interval:

prime_numbers(N, M, Primes) :-
    M > N,
    N1 is N + (M - N) // 2,
    N2 is N1 + 1,
    threaded((
        prime_numbers(N2, M, [], Acc),
        prime_numbers(N, N1, Acc, Primes)
    )).

prime_numbers(N, M, Acc, Primes) :-
    ...

The threaded/1 call terminates when the two implicit thread calls terminate. In a computer with two or more processors (or with a processor with two or more cores) the code above can be expected to provide better computation times when compared with single-threaded code for sufficiently large intervals.

Backtracking over a threaded/1 call is equivalent to backtracking over its argument. For example:

?- threaded(list::(member(X, [1,2,3]), member(Y, [a,b,c]))).

X = 1, Y = a ;
X = 1, Y = b ;
X = 1, Y = c ;
X = 2, Y = a ;
X = 2, Y = b ;
X = 2, Y = c ;
X = 3, Y = a ;
X = 3, Y = b ;
X = 3, Y = c
yes

The threaded/1 built-in predicate is transparent to cuts and exceptions. For example:

?- threaded(list::(member(X, [1,2,3]), !,  member(Y, [a,b,c]))).

X = 1, Y = a ;
X = 1, Y = b ;
X = 1, Y = c
yes

The threaded/1 built-in predicate is most useful for lengthy, independent deterministic computations where the computational costs of each goal outweigh the overhead of the implicit thread creation and management.

Proving goals asynchronously using threads

A goal may be proved asynchronously using a new thread by calling the Logtalk built-in predicate threaded_call/1. The term representing the goal is copied, not shared with the thread. Therefore, any variable bindings resulting from the goal proof must be retrieved by calls to the built-in predicate threaded_exit/1.

The above example with prime numbers can be rewritten using the threaded_call/1 and threaded_exit/1 predicates:

prime_numbers(N, M, Primes) :-
    M > N,
    N1 is N + (M - N) // 2,
    N2 is N1 + 1,
    threaded_call(prime_numbers(N2, M, [], Acc)),
    threaded_call(prime_numbers(N, N1, Acc, Primes)),
    threaded_exit(prime_numbers(N2, M, [], Acc)),
    threaded_exit(prime_numbers(N, N1, Acc, Primes)).

prime_numbers(N, M, Acc, Primes) :-
    ...

Note that a call to the threaded_call/1 predicate is always true (assuming a callable argument). The threaded_exit/1 calls block execution until the results of the threaded_call/1 calls are sent back to the object's thread.

Retrieving asynchronous goal proof results

The results of proving a goal asynchronously in a new thread may be retrieved by calling the Logtalk built-in predicate threaded_exit/1 within the same object where the call to the threaded_call/1 predicate was made.

The threaded_exit/1 predicate allow us to retrieve alternative solutions through backtracking (if you want to commit to the first solution, you may use the threaded_once/1 predicate instead of the threaded_call/1 predicate). For example, assuming a lists object implementing the usual member/2 predicate, we could write:

| ?- threaded_call(lists::member(X, [1,2,3])).

X = _G189 
yes

| ?- threaded_exit(lists::member(X, [1,2,3])).

X = 1 ;
X = 2 ;
X = 3 ;
no

Note that, in this case, the threaded_call/1 and the threaded_exit/1 calls are made within the pseudo-object user. The implicit thread running the lists::member/2 goal suspends itself after providing a solution, waiting for the request of an alternative solution; the thread is automatically terminated when the runtime engine detects that further backtracking to the threaded_exit/1 call is no longer possible.

Calls to the threaded_exit/1 predicate block the caller until the object's thread receives the reply to the asynchronous call. The predicate threaded_peek/1 may be used to check if a reply is already available without removing it from the thread queue. The threaded_peek/1 predicate call succeeds or fails immediately without blocking the caller. However, keep in mind that repeated use of this predicate is equivalent to polling a thread queue, which may severely hurt performance.

Be careful when using the threaded_exit/1 predicate inside failure-driven loops. When all the solutions have been found (and the thread generating them is therefore terminated), re-calling the predicate will block the calling thread, waiting for a reply that most likely will never arrive.

One-way asynchronous calls

Sometimes we want to prove a goal in a new thread without caring about the results. This may be accomplished by using the built-in predicate threaded_ignore/1. For example, assume that we are developing a multi-agent application where an agent may send an "happy birthday" message to another agent. We could write:

threaded_ignore(agent::happy_birthday), ...

The call succeeds with no reply of the goal success, failure, or even exception ever being sent to the object making the call. Note that this predicate implicitly implies a deterministic call of its argument.

Competing goals

The built-in predicate threaded_race/1 allows a disjunction of goals to be interpreted as a set of competing goals, each one running on its own thread. The first thread to terminate successfully leads to the termination of the other threads. This is useful when we have several methods to compute something, without knowing a priori which method will be faster. For example, assume that we have defined a predicate try_method/2 that calls a chosen method, returning its result. We may then write:

method(Result) :-
    ...,
    threaded_race(
        (   try_method(method1, Result)
        ;   try_method(method2, Result)
        ;   ...
        )),
    threaded_exit(try_method(Method, Result)),
    ...

The threaded_exit/1 call will return both the identifier of the fastest method and its result. Note that, when setting up competing goals such as in the example above, we must ensure that the argument of the threaded_exit/1 call unifies with each individual goal in the disjunction used as argument on the threaded_race/1 call. This ensures that the threaded_exit/1 call will return the results of the first method to complete, no matter which one.

Asynchronous calls and synchronized predicates

Proving a goal asynchronously using a new thread may lead to problems when the goal implies side-effects such as input/output operations or modifications to an object's database. For example, if a new thread is started with the same goal before the first one finished its job, we may end up with mixed output, a corrupted database, or unexpected goal failures. In order to solve this problem, predicates (and grammar rule non-terminals) with side-effects can be declared as synchronized by using the synchronized/1 predicate directive. Proving a query to a synchronized predicate (or synchronized non-terminal) is protected by a mutex, thus allowing for easy thread synchronization. For example:

:- synchronized(db_update/1).   % ensure thread synchronization

db_update(Update) :-            % predicate with side-effects
    ...

A second example: assume an object defining two predicates for writing, respectively, even and odd numbers in a given interval to the standard output. Given a large interval, a goal such as:

| ?- threaded_call(obj::odd_numbers(1,1000)), threaded_call(obj::even_numbers(1,1000)).

1 3 2 4 6 8 5 7 10 ...
...

will most likely result in a mixed up output. By declaring the odd_numbers/2 and even_numbers/2 predicates synchronized:

:- synchronized([
    odd_numbers/2,
    even_numbers/2]).

the second goal will only start after the first one finished:

| ?- threaded_call(obj::odd_numbers(1,1000)), threaded_call(obj::even_numbers(1,1000)).

1 3 5 7 9 11 ...
...
2 4 6 8 10 12 ...
...

Note that, in a more realistic scenario, the two threaded_call/1 calls would be made concurrently from different objects. Using the same synchronized directive for a set of predicates imply that they all use the same mutex, as required for this example.

The synchronized/1 directive must precede any local calls to the synchronized predicate (or synchronized non-terminal) in order to ensure proper compilation. In addition, as each Logtalk entity is independently compiled, this directive must be included in every object or category that contains a definition for the described predicate, even if the predicate declaration is inherited from another entity, in order to ensure proper compilation.

Logtalk supports both deterministic and non-deterministic synchronized predicates (and synchronized non-terminals). However, whenever possible, synchronized predicates should be coded as deterministic predicates in order to avoid deadlocks. In those cases where the predicate (or grammar rule) is defined in the same object (or category) where the predicate is declared synchronized, Logtalk takes advantage of any existing mode/2 directives in order to generate the most appropriated mutex handling code. When no mode/2 predicate directives are presented, Logtalk assumes a deterministic predicate when generating the mutex handling code.

We may declare all predicates of an object (or a category) as synchronized by using the entity directive synchronized/0. In this case, the synchronized/1 predicate directive is not necessary and should not be used.

Synchronized predicates may be used as wrappers to messages sent to objects that are not multi-threading aware. For example, assume a random object defining a random/1 predicate that returns random numbers, using side-effects on its implementation. We can specify and define e.g. a sync_random/1 predicate as follows:

:- synchronized(sync_random/1).

sync_random(Random) :-
    random::random(Random).

and then always use the sync_random/1 predicate instead of the predicate random/1 from multi-threaded code.

The synchronization entity and predicate directives may be used when defining objects that may be reused in both single-threaded and multi-threaded Logtalk applications. The directives are simply ignored (i.e. the synchronized predicates are interpreted as normal predicates) when the objects are used in a single-threaded application.

Synchronizing threads through notifications

Declaring a set of predicates as synchronized can only ensure that they are not executed at the same time by different threads. Sometimes we need to suspend a thread not on a synchronization lock but on some condition that must hold true for a thread goal to proceed. I.e. we want a thread goal to be suspended until a condition becomes true instead of simply failing. The built-in predicate threaded_wait/1 allows us to suspend a predicate execution (running in its own thread) until a notification is received. Notifications are posted using the built-in predicate threaded_notify/1. A notification is a Prolog term that a programmer chooses to represent some condition becoming true. Any Prolog term can be used as a notification argument for these predicates. Related calls to the threaded_wait/1 and threaded_notify/1 must be made within the same object, this, as the object's thread message queue is used internally for posting and retrieving notifications.

Each notification posted by a call to the threaded_notify/1 predicate is consumed by a single threaded_wait/1 predicate call. Care should be taken to avoid deadlocks when two (or more) threads both wait and post notifications to each other.