183 lines
No EOL
6.4 KiB
Markdown
183 lines
No EOL
6.4 KiB
Markdown
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 doesn’t 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
|
||
```
|
||
stmp <- 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
|
||
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
|
||
```
|
||
|
||
#### Liveness theorem
|
||
the implementation of the stack is non-blocking
|
||
|
||
*Proof:*
|
||
mi fido |