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
- If a database update requires multiple assert/1
and/or retract/1
operations, a transaction ensure either all are executed or the database
remains unchanged. Notably unexpected exceptions or failures cannot
leave the database in an inconsistent state.
- Other threads do not see the intermediate inconsistent states when a
database update that consists of multiple assert and/or retract is
performed in a transaction. This notably avoids the need to use locks
(see with_mutex/2)
in threads that read the data. A reading thread may still need to use snapshot/1
if a goal depends on multiple calls to dynamic predicates. Unlike locks,
transaction and snapshot based synchronization allows both readers and
writers to make progress simultaneously.89Read-write
locks also provide readers and writers to make progress simultaneously,
but readers see all intermediate states rather than a consistent state.
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
- Transactions allow for “what if” reasoning over the
dynamic database. This is particularly useful when combined with the
deductive database facilities provided by tabling (see section
7).
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.
- transaction(:Goal)
- transaction(:Goal,
+Options)
- Run Goal as once/1
in a transaction. This implies that access to dynamic predicates‘sees’the
dynamic predicates at the moment the transaction is started, together
with the modifications issued by
Goal. Thus, Goal does not see changes to dynamic
predicates from other threads and other threads do not see modifications
by
Goal (isolation). If Goal succeeds, all
modifications become atomically visible to the other threads.
If Goal fails or raises an exception all local modifications
are discarded and transaction/1
fails or passes the exception.
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:
- bulk(+Boolean)
- When
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.
- transaction(:Goal,
:Constraint, +Mutex)
- Similar to transaction/1,
but allows verifying Constraint during the commit phase. This
predicate follows the steps below. Any failure or exception during this
process discards the transaction and releases
Mutex when applicable. Constraint may modify the
database. Such modifications follow the semantics that apply for Goal.
- Call
once(Goal)
- Lock Mutex
- Change the visibility to the current global state combined
with the changes made by Goal
- Call
once(Constraint)
- Commit the changes
- Unlock Mutex.
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),
!.
- snapshot(:Goal)
- Similar to transaction/1,
but always discards the local modifications. In other words, snapshot/1
allows a thread to examine a frozen state of the dynamic predicates
and/or make isolated modifications without affecting other threads and
without making permanent changes to the database. Where transactions
allow the global state to be updated atomically from one consistent
state to the next, a snapshot allows reasoning about a consistent state.
- [nondet]current_transaction(-Goal)
- True when called inside a transaction running Goal. This
predicate generates candidates from the current (nested) transaction
outward.
Goal is a plain goal if the calling context module is the
same as matching transaction/1
or snapshot/1
and a qualified callable term otherwise. Note that this only enumerates
transactions in the current thread.
- transaction_updates(-Updates)
- Unify Updates with a list of database updates that would be
effectuated if the transaction is going to be committed at this stage.
Updates is a list of terms defined below. The elements are
sorted on the change generation, i.e., the order in which the operations
were performed.
- asserta(+ClauseRef)
- assertz(+ClauseRef)
- The given clause will be asserted at the start or end. Note that due to
competing transactions the clause may no longer be the first/last clause
of the predicate.
- erased(+ClauseRef)
- The given clause will be removed. This may be due to erase/1,
retract/1
or retractall/1.