272 lines
No EOL
6.7 KiB
Markdown
272 lines
No EOL
6.7 KiB
Markdown
**Object:** entity with an implementation (hidden) and an interface (visible), made up of a set of operations and a specification of the behavior.
|
|
**Concurrent:** if the object can be accessed by different processes.
|
|
|
|
**Semaphore:** is a shared counter S accessed via primitives $up$ and $down$ s.t.:
|
|
- is initialized at s0 >= 0
|
|
- it is alwayz >= 0
|
|
- *up* atomically increases S
|
|
- *down* atomically decreases S if it is not 0, otherwise the calling process is blocked and waits.
|
|
|
|
Main use: **prevent busy waiting**: suspend processes that cannot perform *down*.
|
|
|
|
**Strong semaphore:** if uses a FIFO policy for blocking/unblocking, otherwise it's **weak**.
|
|
**Binary semaphore:** if it is at most 1 (also *up* are blocking).
|
|
|
|
A semaphore needs two underlying objects:
|
|
- a counter initialized at s0 that can also become negative (see the implementation below to understand)
|
|
- a data structure (typically a queue), initially empty, to store suspended processes.
|
|
|
|
#### Ideal implementation
|
|
```
|
|
S.down() :=
|
|
S.counter--
|
|
if S.counter < 0 then
|
|
enter into S.queue
|
|
SUSPEND
|
|
return
|
|
|
|
S.up() :=
|
|
S.counter++
|
|
if S.counter <= 0 then
|
|
activate a proc from S.queue
|
|
return
|
|
```
|
|
>[!note] note
|
|
>if S.counter ≥ 0, then this is the value of the semaphore; otherwise, S.counter tells you how many processes are suspended in S
|
|
>
|
|
>all operations are in MUTEX
|
|
|
|
|
|
#### Actual implementation
|
|
```
|
|
Let t be a test&set register initialized at 0
|
|
|
|
S.down() :=
|
|
Disable interrupts
|
|
wait S.t.test&set() = 0
|
|
S.counter--
|
|
if S.counter < 0 then
|
|
enter into S.queue
|
|
S.t <- 0
|
|
Enable interrupts
|
|
SUSPEND
|
|
else
|
|
S.t <- 0
|
|
Enable interrupts
|
|
return
|
|
|
|
S.up() :=
|
|
Disable interrupts
|
|
wait S.t.test&set() = 0
|
|
S.counter++
|
|
if S.counter <= 0 then
|
|
activate a proc from S.queue
|
|
S.t <- 0
|
|
Enable interrupts
|
|
return
|
|
```
|
|
Same as before but we use test&set to actually ensure MUTEX (of course we could have used any other hardware MUTEX implementation seen so far).
|
|
|
|
#### (Single) Producer/Consumer
|
|
It's a shared FIFO buffer of size k.
|
|
- `BUF[0,…,k-1]`: generic registers (not even safe) accessed in MUTEX
|
|
- `IN/OUT` : two variables pointing to locations in `BUF` to (circularly) insert/remove items, both initialized at 0
|
|
- `FREE/BUSY`: two semaphores that count thew number of free/busy cells of `BUF`, initialized at k and 0 respectively.
|
|
|
|
```
|
|
B.produce(v) :=
|
|
FREE.down() # blocking if FREE becomes negative
|
|
BUF[IN] <- v
|
|
IN <- (IN+1) mod k # mod k since it is circular
|
|
BUSY.up() # "there is something to consume"
|
|
return
|
|
|
|
B.consume() :=
|
|
BUSY.down() # it blocks if there is nothing to consume
|
|
tmp <- BUF[OUT]
|
|
OUT <- (OUT+1) mod k
|
|
FREE.up()
|
|
return tmp
|
|
```
|
|
|
|
> [!note] Downside
|
|
> Reading from / writing into the buffer can be very expensive!
|
|
|
|
#### (Multiple) Producers/Consumers
|
|
**Accessing BUF in MUTEX slows down the implementation**, we would like to have the possibility to read and write from different cells **in parallel**.
|
|
- we use two arrays FULL and EMPTY of atomic boolean registers, initialized at ff (all zeros) and tt (all ones), respectively
|
|
- we have two extra semaphores SP and SC, both initialized at 1
|
|
|
|
```
|
|
B.produce(v) :=
|
|
FREE.down()
|
|
SP.down()
|
|
while not EMPTY[in] do
|
|
IN <- (IN+1) mod k
|
|
i <- IN
|
|
EMPTY[IN] <- ff
|
|
SP.up()
|
|
BUF[i] <- v
|
|
FULL[i] <- tt
|
|
BUSY.up()
|
|
return
|
|
|
|
B.consume() :=
|
|
BUSY.down()
|
|
SC.down()
|
|
while not FULL[OUT] do
|
|
OUT <- (OUT+1) mod k
|
|
o <- OUT
|
|
FULL[OUT] <- ff
|
|
SC.up()
|
|
tmp <- BUF[o]
|
|
EMPTY[o] <- tt
|
|
FREE.up()
|
|
return tmp
|
|
```
|
|
Thanks to the semaphores, we are sure that while loops will not go on forever! The loops starts only if there is at least a FREE / BUSY cell.
|
|
|
|
#### (Multiple) Producers/Consumers - Wrong solution
|
|
**EXERCISE** this example is wrong
|
|
```
|
|
B.produce(v) :=
|
|
FREE.down()
|
|
SP.down()
|
|
i <- IN
|
|
IN <- (IN + 1) mod k
|
|
EMPTY[in] <- ff
|
|
SP.up()
|
|
BUF[i] <- v
|
|
FULL[i] <- tt
|
|
BUSY.up()
|
|
return
|
|
|
|
B.consume() :=
|
|
BUSY.down()
|
|
SC.down()
|
|
o <- OUT
|
|
OUT <- (OUT+1) mod k
|
|
FULL[OUT] <- ff
|
|
SC.up()
|
|
tmp <- BUF[o]
|
|
EMPTY[o] <- tt
|
|
FREE.up()
|
|
return tmp
|
|
```
|
|
|
|
#### The Readers/Writers problem
|
|
- Several processes want to access a file
|
|
- Readers may simultaneously access the file
|
|
- At most one writer at a time
|
|
- Reads and writes are mutually exclusive
|
|
|
|
>[!note] Remark
|
|
>this generalizes the MUTEX problem (MUTEX = RW with only writers)
|
|
|
|
The read/write operations on the file will all have the following shape:
|
|
```
|
|
conc_read() :=
|
|
begin_read()
|
|
read()
|
|
end_read()
|
|
|
|
conc_write() :=
|
|
begin_write()
|
|
write()
|
|
end_write()
|
|
```
|
|
|
|
##### Weak priority to Readers
|
|
- If a reader arrives during a read, it can surpass possible writers already suspended (risk of starvation for the writes)
|
|
- When a writer terminates, it activates the first suspended process, irrispectively of whether it is a reader or a writer (so, the priority to readers is said «weak»)
|
|
|
|
```
|
|
GLOB_MUTEX and R_MUTEX semaphores init. at 1
|
|
R a shared register init. at 0
|
|
|
|
begin_read() :=
|
|
R_MUTEX.down()
|
|
R++ # currently active readers
|
|
if R = 1 then GLOB_MUTEX.down()
|
|
R_MUTEX.up()
|
|
return
|
|
|
|
end_read() :=
|
|
R_MUTEX.down()
|
|
R--
|
|
if R = 0 then GLOB_MUTEX.up()
|
|
R_MUTEX.up()
|
|
return
|
|
|
|
begin_write() :=
|
|
GLOB_MUTEX.down()
|
|
return
|
|
|
|
end_write() :=
|
|
GLOB_MUTEX.up()
|
|
return
|
|
```
|
|
If readers keeps arriving, they surpass writers. But when a writer terminates it activates the first suspended process, which can be a reader or a writer.
|
|
##### Strong priority to Readers
|
|
When a writer terminates, it activates the first reader, if there is any, or the first writer, otherwise.
|
|
|
|
```
|
|
GLOB_MUTEX and R_MUTEX and W_MUTEX semaphores init. at 1
|
|
R a shared register init. at 0
|
|
|
|
begin_read() := end_read() :=
|
|
come prima come prima
|
|
|
|
begin_write() :=
|
|
W_MUTEX.down()
|
|
GLOB_MUTEX.down()
|
|
return
|
|
|
|
end_write() :=
|
|
GLOB_MUTEX.up()
|
|
W_MUTEX.up()
|
|
return
|
|
```
|
|
This is prioritizing readers as `GLOB_MUTEX.up()` is called before `W_MUTEX.up()`, this is stronger that what it is done before.
|
|
##### Weak priority to Writers
|
|
```
|
|
GLOB_MUTEX, PRIO_MUTEX (to prioritize the writers), R_MUTEX and W_MUTEX semaphores init. at 1
|
|
R and W shared registers init. at 0
|
|
|
|
begin_read() :=
|
|
PRIO_MUTEX.down()
|
|
R_MUTEX.down()
|
|
R++
|
|
if R = 1 then
|
|
GLOB_MUTEX.down()
|
|
R_MUTEX.up()
|
|
PRIO_MUTEX.up()
|
|
return
|
|
|
|
end_read() := # like weak priority to readers
|
|
R_MUTEX.down()
|
|
R--
|
|
if R = 0 then GLOB_MUTEX.up()
|
|
R_MUTEX.up()
|
|
return
|
|
|
|
def begin_write() :=
|
|
W_MUTEX.down()
|
|
W++
|
|
if W = 1 then
|
|
PRIO_MUTEX.down()
|
|
W_MUTEX.up()
|
|
GLOB_MUTEX.down()
|
|
return
|
|
|
|
def end_write() :=
|
|
GLOB_MUTEX.up()
|
|
W_MUTEX.down()
|
|
W--
|
|
if W = 0 then
|
|
PRIO_MUTEX.up()
|
|
W_MUTEX.up()
|
|
return
|
|
```
|
|
This is prioritizing writers as if there are writers waiting, they will be waiting at `GLOB_MUTEX.down()`. This semaphore is upped before `PRIO_MUTEX` which is the one that blocks readers.
|
|
But writers won't be able to writer until there are no readers, if they keep coming, they will block the writers as `GLOB_MUTEX` will never be upped. |