Traditionally, Prolog database updates add or remove individual clauses. The Logical Update View ensures that a goal that is started on a dynamic predicate does not see modifications due to assert/1 or retract/1 during its life time. See section 4.14.5. In a multi-threaded context this assumption still holds for individual predicates: concurrent modifications to a dynamic predicate are invisible.
Transactions allow running a goal in isolation. The goals running inside the transaction‘see' the database as it was when the transaction was started together with database changes done by the transaction goal. Other threads see no changes until the transaction is committed. The commit, also if it involved multiple clauses spread over multiple predicates, becomes atomically visible to other threads. Transactions have several benefits Wielemaker, 2013
Transactions on their own do not guarantee consistency. For example, when running the code below to update the temperature concurrently from multiple threads it is possible for the global state to have multiple temperature/1 clauses.
update_temperature(Temp) :- transaction(( retractall(temperature(_)), asserta(temperature(Temp)))).
Global consistency can be achieved by wrapping the above transaction using with_mutex/2 or by using transaction/3 with a constraint that demands a single clause for temperature/1
SWI-Prolog transactions only affect the dynamic database. Static predicates are globally visible and shared at all times. In particular, transactions do not affect loading source files and thus, source files loaded inside a transaction (e.g., due to autoloading) are immediately globally visible. This may pose problems if loading source files provide clauses for dynamic predicates.
Currently the number of database changes inside a transaction (or
snapshot, see snapshot/1)
is limited to 2 ** 32 -1. If this limit is exceeded a representation_error(transaction_generations)
exception is raised.
Transactions may be nested. The above mentioned limitation for the number of database changes applies to the combined number in nested transactions.
If Goal succeeds, the transaction is committed. This implies that (1) any clause that is asserted in the transaction and not retracted in the same transaction is made globally visible and (2) and clause the existed before the transaction and is retracted in the transaction becomes globally invisible. Multiple transactions may retract the same clause and be committed, i.e., committing a retract that was already performed is a no-op. All modifications become atomically visible to other threads. The transaction/3 variation allows for verifying constraints just before the commit takes place.
Clause ordering Inside a transaction clauses can be added using asserta/1 and assertz/1. If only a single transaction is active at any point in time transactions preserve the usual ordering of clauses. However, if multiple transactions manipulate the same predicate(s) concurrently (typically using transaction/3), the final order of the clauses is the order in which the transactions asserted the clauses and not the order in which the transactions are committed.
The transaction/1
variant is equivalent to transaction(Goal,[])
. The transaction/2
variant processed the following options:
true
, accumulate events from changes to dynamic
predicates (see prolog_listen/2)
and trigger these events as part of the commit phase. This implies that
if the transaction is not committed the events are never triggered.
Failure to trigger the events causes the transaction to be discarded.
Experimental.
once(Goal)
once(Constraint)
This predicate is intended to execute multiple transactions with a time consuming Goal in part concurrently. For example, it can be used for a Compare And Swap (CAS) like design. We illustrate this using a simple counter in the code below. Note that the transaction fails if some other thread concurrently updated the counter. This is why we need the repeat/0 and a final !/0. The CAS-style update is in general useful if Goal is expensive and conflicts are rare.
:- dynamic counter/1. increment_counter(Delta) :- repeat, transaction(( counter(Value), Value2 is Value+Delta, ), ( retract(counter(Value)), asserta(counter(Value2)) ), counter_lock), !.