Selective Read-Locks
Concurrency Optimisation by Selective Read-Locks
An application may optimise locking to obtain a concurrency advantage by setting shared locks on objects which it is known will not be changed in the transaction, so that those objects, being read-locked, remain accessible to concurrent read-only transactions. Note, however, that if such an object is inadvertently changed and included in the update read-write transaction, then the commit will fail.
In transactions which read many objects but change only a few, it will usually be more convenient to use a read-only transaction than to set explicit read-locks.
Using a preliminary read-only transaction
Write-locking of, for example, global collections can be minimised by the use of a preliminary read-only transaction to select from the collection only those objects which are to be updated (assuming the object’s key in a dictionary is not to be changed, which would require a write-lock on the dictionary), and assign them to temporary variables for use in a subsequent read-write update transaction, in which they will be write-locked, but in which the collection via which they were found will not be touched.
In this technique, the preliminary read-only transaction will rollback and release its locks before the update transaction begins. Of the four variants of this technique presented here, this one has the minimum lock contention, but will break ACID rules if the definition of the transaction (which is the responsibility of the application designer, not definable by any DBMS), is such that it makes assumptions about the to-be-updated-objects’ subsequent membership of the collection, rather than just as a means to find the objects to be updated.
Nesting in a read-only transaction with short read-locks
An outer read-only transaction setting short read-locks (#isolationDegree = 2) will allow greater concurrency than with long read-locks as in the next technique variant below, and if VOSS is running on the desktop client and is accessing the virtual spaces on a fileserver, then a browse/update window wrapped in an open-ended read-only transaction with short read-locks will have minimal impact on other users, and in particular, the browse/update user interface window’s widgets may safely send their close-down messages to their virtual object contents, as this will be within the context of the enclosing read-only transaction, after the inner update transaction has committed, rather than falling into the default Top transaction, which should be considered a programming error.
If, however, any object which has been short read-locked in the outer read-only transaction is subsequently, and perhaps unavoidably, sent a message in the inner read-write transaction, then it will be write-locked, as the short read-lock will by then have been released, and this may nullify the intended concurrency optimisation.
Nesting in a read-only transaction with long read-locks
If it is inconvenient to ensure that the inner read-write transaction does not send messages to objects which have been read-locked in the outer transaction, then set the outer transaction’s isolationDegree=3 (read-locks held until rollback) and set the inner transaction’s #acceptObjectsReadLockedBySuperTransaction attribute to <false>.
The outer read-only transaction may then access objects to be read-locked by sending any message (if necessary, just #setLock will make the intention clear in the source code) before the inner read-write transaction begins. In this technique, the read-locks will be held until the enclosing read-only transaction rolls back, after the inner update transaction has committed, thus maintaining ACID consistency, and since objects read-locked in the outer transaction will not be accepted into the inner update transaction, no matter how many messages are sent to them, it doesn’t matter if they are ‘inadvertently’ changed - they will not be included in the commit.
Note that for this to work, the enclosing read-only transaction must be set to #isolationDegree = 3 (against the default value of 2) like this:
[:roTxn |
roTxn isolationDegree: 3.
…
[:rwTxn |
rwTxn acceptObjectsReadLockedBySuperTransaction: false.
…
] atomic
] atomicReadOnly
If the outer read-only transaction were to be left with the default short read-locks then it would release its read-locks before the read-write transaction began and the read-write transaction would then set its write-locks as usual.
When using this optimisation technique, take care to ensure that no object which is to be changed is inadvertently sent a prior message in the outer read-only transaction, since this would cause it to be excluded from the commit without warning.
This can be checked during development testing by temporarily setting #acceptObjectsReadLockedBySuperTransaction <true>, which will force the inner read-write transaction to abort its commit during testing, which may be optionally notified in the system message log file <vspace>.log and the Transcript, as above, if any read-locked object in it has been changed.
Setting selective read-locks in a read-write transaction
If the number of objects to be read-locked is small then this fourth concurrency optimisation technique may be more convenient: Instead of using a preliminary or enclosing read-only transaction, selective read-locks may be set on objects within the read-write update transaction itself, either by temporarily setting its attribute #exclusiveLocks <false>, or by explicit sending of #setReadLock as the first message to each such object in that transaction.
The latter of those two methods however, whilst simpler, will read-lock only the object which the message #setReadLock is sent to, and if that object is, for example, a VirtualDictionary (which is just a wrapper for a tree of virtual Btree nodes), then only the instance of VirtualDictionary (the wrapper) will be read-locked, and a subsequent lookup message (e.g. #at:ifAbsent:) will set write-locks on all the interior nodes which lie on the path from the root node to the leaf node where the required object is located, and in particular, since the root node will be write-locked, the whole dictionary will effectively be write-locked. In this case, the transaction will behave normally and release all locks on commit, but other read-only transactions in the meantime will have had to wait for the write-locked root node of the dictionary as though no optimisation had been attempted.
Temporary manipulation of the #exclusiveLocks attribute around the intended read-only accesses is the safer technique, and can also set short read-locks, like this:
| myObject myDictionary exclLocks isoDeg |
[:thisTxn |
…
[ exclLocks := thisTxn exclusiveLocks. “save current values”
isoDeg := thisTxn isolationDegree.
thisTxn
exclusiveLocks: false;
isolationDegree: 2.
myObject := myDictionary at: ‘myKey’ ifAbsent: [] “set & release short readlocks”
] ensure: “restore old transaction attribute values”
[ thisTxn
exclusiveLocks: exclLocks;
isolationDegree: isoDeg
].
…
myObject doSomething.
…
] atomic
