master-degree-notes/Concurrent Systems/notes/7- MUTEX-free concurrency.md

185 lines
No EOL
6.5 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.

Critical sections (locks) have drawbacks:
- if not put at the right level of granularity, they unnecessarily reduce concurrency
- delays of one process may affect the whole system
**MUTEX-freedom:** the only atomicity is the one provided by the primitives themselves (no wrapping of code into CSs)
the liveness properties used so far cannot be used anymore, since they rely on CSs.
(example: if we have only atomic R/W registers, these are the only atomic things that we have. But we may also have atomic primitives like *test&set*, *compare&swap* ecc.).
#### Liveness properties
We have four new liveness properties
1. **Obstruction freedom:** every time an operation is run in isolation (no overlap with any other operation on the same object), it terminates
2. **Non-blocking:** whenever an operation is invoked on an object, eventually one operation on that object terminates
- reminds deadlock-freedom in MUTEX-based concurrency
3. **Wait freedom:** whenever an operation is invoked on an object, it eventually terminates
- reminds starvation-freedom in MUTEX-based concurrency
4. **Bounded wait freedom:** wait freedom + a bound on the number of steps needed to terminate
- reminds bounded bypass in MUTEX-based concurrency
*REMARK:* these notions naturally cope with (crash) failures. Fail stop is another way of terminating, there is no way of distinguishing a failure from an arbitrary long sleep (because of asynchrony).
### A wait-free Splitter
Assume we have atomic R/W registers.
A **splitter** is a concurrent object that provides a single operation **dir** such that:
1. (*validity*) it returns L, R or S (left, right, stop)
2. (*concurrency*) in case of n simultaneous invocations of **dir**
1. at most n-1 L are returned
2. at most n-1 R are returned
3. at most 1 S is returned
3. (*wait freedom*) it eventually terminates.
Idea:
- not all processes obtain the same value
- in a solo execution (i.e., without concurrency) the invoking process must stop (0 L && 0 R && at most 1 S)
We have:
- DOOR: MRMW boolean atomic register initialized at 1
- LAST: MRMW atomic register initialized at whatever process index
```
dir(i) :=
LAST <- i
if DOOR = 0 then
return R
else
DOOR <- 0
if LAST = i then
return S
else
return L
```
With 2 processes, we can have:
- one goes left and one goes right
- one goes left and the other stops
- one goes right and the other stops
#### Soundness theorem
this implementation satisfies the three requirements for the splitter
*Proof:*
1. Not all processes can obtain R
- the door must have been closed and who closed the door cannot obtain R
2. not all processes can obtain L
- let us consider the last process that writes into LAST (this is an atomic register, so this is meaningful)
- if the door is closed, it receives R and √
3. let $p_i$ be the first process that receives $S \to LAST=i$ in its second if
![[Pasted image 20250324091452.png]]
### An Obstruction-free Timestamp Generator
A **timestamp generator** is a concurrent object that provides a single operation get_ts such that:
1. (*validity*) not two invocations of get_ts return the same value
2. (*consistency*) if one process terminates its invocation of get_ts before another one starts, the first receives a timestamp that is smaller than the one received by the second one
3. (*obstruction freedom*) if run in isolation, it eventually terminates
Idea: use something like a splitter for possible timestamp, so that only the process that receives S (if any) can get that timestamp.
We have:
- `DOOR[i]`: MRMW boolean atomic register initialized at 1, for all i
- `LAST[i]`: MRMW atomic register initialized at whatever process index, for all i
- NEXT: integer initialized at 1
```
get_ts(i) :=
k <- NEXT
while true do
LAST[k] <- i
if DOOR[k] = 1 then
DOOR[k] <- 0
if LAST[k] = i then
NEXT++
return k
k++
```
#### Soundness theorem (yes, again)
this implementation satisfies the three properties of the timestamp generator
1. Validity holds because of property 2.c of the splitter
2. For consistency, the invocation that terminates increased the value of NEXT before terminating
- every process that starts after its termination will find NEXT to a greater value (NEXT never decreases!)
3. Obstruction freedom is trivial
**REMARK:** this implementation doesnt satisfy the non-blocking property:![[Pasted image 20250324092633.png]]
### A Wait-free Stack
REG is an unbounded array of atomic registers (the stack)
For all i, `REG[i]` can be:
- written
- read by the `swap(v)` primitives (that atomically writes a new value in it)
- initialized at `⊥`
NEXT is an atomic register (pointing at the next free location of the stack) that can be
- read
- `fetch&add`
- initialized at 1
```
push(v) :=
i <- NEXT.fetch&add(1)
REG[1] <- v
pop() :=
k <- NEXT-1
for i = k down to 1
tmp <- REG[1].swap(⊥)
if tmp != ⊥ then
return tmp
return ⊥
```
REMARK: crashes do not compromise progress!
PROBLEM: unboundedness of REG is not realistic
### A Non-blocking Bounded Stack
Idea: every operation is started by the invoking process and finalized by the next process
`STACK[0...k]` : an array of registers that can be read or compare&setted
`STACK[i]` is actually a pair ⟨val , seq_numb⟩ initialized at ⟨⊥,0⟩
This is needed for the so called ABA problem with compare&set:
- A typical use of compare&set is
```
tmp <- X
...
if X.compare&set(tmp, v) then ...
```
- this is to ensure that the value of X has not changed in the computation
- the problem is that X can be changed twice before compare&set (e.g. it was A, it became B and than came back to A)
- solution: X is a pair ⟨val , seq_numb⟩, with the constraint that each modification of X increases its sequence_number
- with the compare&set you mainly test that the sequence_number has not changed
TOP : a register that can be read or compare&setted
![[Pasted image 20250324100652.png]]
```
push(w) :=
while true do
⟨i,v,s⟩ <- TOP
conclude(i,v,s)
if i=k then
return FULL
else
newtop <- ⟨i+1,w,STACK[i+1].seq_num+1⟩
if TOP.compare&set(⟨i,v,s⟩,newtop) then
return OK
conclude(i, v, s) :=
tmp <- STACK[i].val
STACK[i].compare&set(⟨tmp,s-1⟩,⟨v,s⟩)
pop() :=
while true do
⟨i,v,s⟩ <- TOP
conclude(i,v,s)
if i=0 then
return EMPTY
newtop <- ⟨i-1,STACK[i-1]⟩
if TOP.compare&set(⟨i,v,s⟩,newtop) then
return v
```
la conclude serve a garantire che il sequence number sia corretto. Se è stato già aggiornato non lo aggiorna.
#### Liveness theorem
the implementation of the stack is non-blocking
*Proof:*
mi fido