master-degree-notes/Concurrent Systems/notes/4 - Semaphores.md

6.7 KiB

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.