master-degree-notes/Concurrent Systems/notes/5 - Software Transactional Memory.md

182 lines
No EOL
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

z- Group together parts of the code that must look like atomic, in a way that is transparent, scalable and easy-to-use for the programmer
- Differently from monitors, the part of the code to group is not part of the definition of the objects, but is application dependent
- Differently from transactions in databases, the code can be any code, not just queries on the DB
**Transaction:** an atomic unit of computation (look like instantaneous and without overlap with any other transaction), that can access atomic objects.
when executed alone, every transaction successfully terminates.
**Program:** set of sequential processes, each alternating transactional and non-transactional code
**STM system:** online algorithm that has to ensure the atomic execution of the transactional code of the program.
To guarantee efficiency, all transactions can be executed at the same time (optimistic execution approach), but they must be totally ordered
- not always possible (where there are different accesses to the same object, with at least one of them that changes it)
- commit/abort transactions at their completion point (or even before)
- in case of abort, either try to re-execute or notify the invoking proc.
- possibility of unbounded delay
Conceptually, a transaction is composed of 3 parts:
`[READ of atomic regs] [local comput.] [WRITE into shared memory]`
The key issue is ensuring consistency of the shared memory
- as soon as some inconsistencies is discovered, the transaction is aborted
Implementation: every transaction uses a local working space
- For every shared register: the first READ copies the value of the reg. in the local copy; successive READs will then read from the local copy
- Every WRITE modifies the local copy and puts the final value in the shared memory only at the end of the transaction (if it has not been aborted)
4 operations:
- `begin_T()`: initializes the local control variables
- `X.read_T(), X.write_T()`: described above
- `try_to_commit_T()`: decides whether a transaction (non-aborted) can commit
#### A Logical Clock based STM system
All the READs perform if no inconsistencies arise, or before any inconsistency
(definizioni in def qua sotto)
>[!def]
>Let T be a transaction; its read prefix is formed by all its successful READ before its possible abortion.
>[!def]
>An execution is **opaque** if all committed transactions and all the read prefixes of all aborted transactions appear if executed one after the other, by following their real-time occurrence order.
We now present an atomic STM system, called *Transactional Locking 2*:
- CLOCK is an atomic READ/FETCH&ADD register initialized at 0
- Every MRMW register X is implemented by a pair of register XX s.t.
- XX.val contains the value of X
- XX.date contains the date (in terms of CLOCK) of the last update
- it is associated with a lock object to guarantee MUTEX when updating the shared memory
- For every transaction T, the invoking process maintains
- `lc(XX)`: a local copy of the implementation of reg. X
- `read_set(T)`: the set of names of all the registers read by T up to that moment
- `write_set(T)`: the set of names of all the registers written by T up to that moment
- `birthdate(T)`: the value of CLOCK(+1) at the starting of T
**Idea:** commit a transaction if and only if (iff) it could appear as executed at its birthdate time
**Consistency:**
- if T reads X, then it must be that `XX.date < birthdate(T)`
- to commit, all registers accessed by T cannot have been modified after T's birthdate (again, `XX.date < birthdate(T)`)
###### Implementation:
```
begin_T() :=
read_set(T), write_set(T) <- ∅
birthdate(T) <- CLOCK + 1
X.read_T() :=
if l?(XX) != ⊥ then
return lc(XX).val
lc(XX) <- XX
if lc(XX).date >= birthdate(T) then
ABORT
read_set(T) <- read_set(T) U {X}
return lc(XX).val
X.write_T(v) :=
if lc(XX) = ⊥ then
lc(XX) <- newloc
lc(XX).val <- v
write_set(T) <- write_set(T) U {X}
try_to_commit_T() :=
lock all read_set(T) U write_set(T)
∀ X ∈ read_set(T)
if XX.date >= birthdate(T) then
release all locks
ABORT
tmp <- CLOCK.fetch&add(1)+1
∀ X ∈ write_set(T)
XX <- ⟨lc(XX).val, tmp⟩
release all locks
COMMIT
```
### Virtual World Consistency
Opacity requires a total order on all committed transactions and on all read prefixes of all aborted transactions
this latter requirement can be weakened by imposing that the read prefix of an aborted transaction is consistent only w.r.t its casual past (i.e. its virtual world).
**Opacity:** total order both on all committed transactions and on read prefixes of aborted transactions
**VWC:** total order on all committed transactions + partial order on committed transactions and the read prefixes of aborted transactions.
The **casual past** of a transaction T is the set of all T' and T'' such that
- T reads a value written by T'
- T'' belongs to the casual past of T'
VWC allows more transactions to commit -> it is a more liberal property than opacity.
![[Pasted image 20250317105355.png]]
#### A Vector clock based STM system
We have m shared MRMW registers; register X is represented by a pair XX, with:
- `XX.val` the current value of X
- `XX.depend[1...m]` a vector clock s.t.
- `XX.depend[X]` is the sequence number associated with the current value of X
- corresponds to the `date` of the previous algorithm
- `XX.depend[Y]` is the sequence number associated with the value of Y on which the current value of X depends from
- There is a starvation-free lock object associated to the pair
So for X, I don't just have to track "who modified X", but also "who modified "Y or Z", if they may have influenced X.
We have n processes; process $p_i$ has
- for every X, a local copy `lc(XX)` of the implementation of X
- $p\_depend_i[1…m]$ s.t. $p\_depend_i[X]$ is the sequence number of the last value of X (directly or indirectly) known by $p_i$
Every transaction T issues by $p_i$ has:
- `read_set(T)` and `write_set(T)`
- $t\_depend_{T}[1…m]$ a local copy of $p\_depend_i$ (this is used in the optimistic execution, not to change $p\_depend_{i}$ if T aborts)
```
begin_T(i) :=
read_set(T), write_set(T) <- ∅
t_depend_T <- p_depend_i
X.read_T(i) :=
if lc(XX) = ⊥ then
lc(XX) <- newloc
lc(XX) <- XX
read_set(T) <- read_set(T) U {X}
t_depend_T[X] <- lc(XX).depend[X]
if ∃ Y ∈ read_set(T) s.t. t_dependT[Y] < lc(XX).depend[Y] then
# significa che il valore di X che ho letto è stato influenzato
# da una modifica di Y avvenuta dopo che T (questa trans.) ha letto Y
# per cui significa che X è stato scritto dopo che T ha letto Y
# da una trans. che ha scritto sia X che Y
ABORT
∀ Y ∉ read_set(T) do
t_depend_T[Y] <- max{t_depend_T[Y], lc(XX).depend[Y]}
# aggiorno la dipendenza da Y della transazione
# perché solo per quelli NON nel read_set?
# perché se dovessi farlo con un Y nel read_set dovrei abortire
# per i motivi di sopra! (e quindi in caso avrei già abortito)
return lc(XX).val
X.write_T(i, v) :=
if lc(XX) = ⊥ then
lc(XX) <- newloc
lc(XX).val <- v
write_set(T) <- write_set(T) U {X}
try_to_commit_T(i) :=
lock all read_set(T) U write_set(T)
if ∃ Y ∈ read_set(T) s.t. t_dependT[Y] < YY.depend[Y] then
release all locks
ABORT
# abortisco se ho letto un Y che è stato modificato dopo che io l'ho letto
∀ X ∈ write_set(T) do
t_dependT[X] <- XX.depend[X]+1
∀ X ∈ write_set(T) do
XX <- ⟨lc(XX).val, t_dependT⟩
release all locks
p_depend_i <- t_depend_T
COMMIT
```