Solutions
CS solutions can be classified as follows:
- Software Mutex
- Algo
- Software
- Busy waits
- Constrained
- Hardware Spinlocks:
- Busy waits
- Software spinlocks and mutex locks
- Semaphores
- No busy waiting
- Generalisation of mutex locks
- Able to protect two or more identical shared resources
- Conditional Variables
- No busy waiting
- Wake up condition
Software Mutex¶
Peterson's Solution¶
Conditions:
- Two processes only (can be extended to N processes with proper data struct)
- Practical only for single processor system
LD/ST
MUST be atomic. An atomic operation completes "in a single step" w.r.t. other threads. An atomicST
means that no other process can read/write to a half written value.
let int turn
and bool flag[2]
be two shared global variables
flag = {false, false}
and turn = arbitrary value
The code for process Pi
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The algorithm above is the solution for process Pi
. The code for Pj
is equivalent, just swap all the i
to j
and vice versa.
Let's consider a few variations of this algorithm that one might think of:
Why do we need the flag?¶
1 2 3 4 5 6 7 8 9 10 11 |
|
This does not work, because imagine that this same process wants to re-enter the critical section, but the other process has left its critical section and will not re-enter it at all. In this case, the other process won't set the turn = i
and this process will never be able to re-enter the CS. It violates progress!
Why do we need the turn?¶
1 2 3 4 5 6 7 8 9 10 11 |
|
This does not work, because imagine that both processes want to enter the CS. process i
set its flag[i] = true
then gets interrupted, and process j
is scheduled. Now process j
sets flag[j] = true
. Now we have a deadlock. This violates progress as well!
Thus, we need both turn
and flag
for the algorithm to work correctly.
A few variations that do work:¶
This is nothing but the same thing expressed in different "words"
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Here turn
is not a good name for the variable:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
To check if a solution is correct?¶
- Check for mutex: can another process enter CS while one is already in CS?
- Check for progress: can another process enter CS if the other process has left the CS and won't try to re-enter it
- Check for bounded waiting: can a process re-enter the CS after just leaving it?
Synchronisation hardware¶
Number of hardware locks may be physically limited
Preventing interrupts¶
We can intentionally prevent interrupts from occurring while a shared variable is being modified. This only works in a single processor system and is not feasible on a multiprocessor architecture, because:
- Time consuming, need to pass a message to all other processors
- Clock synchronisation between processors
- Decreases efficiency, defeats the purpose of multiprocessing
Atomic instructions¶
Atomic instructions can be implemented by locking the memory bus. mutex is trivial. Typically used for synchronisation in SMP systems. Common instructions are:
compare_and_swap()
test_and_set()
read
andwrite
swap
fetch_and_add
load_link
/store_conditional
Software Spinlocks and Mutex locks¶
This uses hardware synchronisation instructions like the ones mentioned above. Although, this requires more memory than a hardware spinlock
Spinlocks¶
A spinlock provides mutex. It is simply a variable that is repeatedly checked for availability using atomic instructions:
1 2 3 4 5 6 7 8 9 |
|
Busy Waiting¶
wastes CPU cycles. In a single processor environment, this is wasteful as no other process is able to run if there is busy wait.
spinlock is useful in a multiprocessor system when the anticipated waiting time is shorter than a quantum. The advantage over mutex lock here is that there is a context switch with mutex locks, since this thread must be put to sleep first, and if the waiting time is short, this is more wasteful than spinning.
Mutex Lock¶
Requires integration with the kernel scheduler since it puts the waiting process to sleep to reduce busy wait. Context switch is involved, and thus this is an expensive operation to perform. should only be done when the waiting time is much longer than the time used for context switching
Semaphores¶
Generalised Mutex Lock, implemented at the kernel level. This is a high level software solution that relies on synchronisation hardware and is considered more robust than mutex locks. These do not cause busy waiting either.
A semaphore is defined as:
int numAvailableResources
whcih is defined in the kernel.- accesible only via
signal()
andwait()
Semaphores can be used in two ways:
- Binary semaphore (0 or 1)
- Counting semaphore (1 to N)
How they work: Semaphores integrate with the kernel scheduler to avoid busy waiting by putting the threads to sleep if there are no available resources.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
note: wait()
and signal()
are critical sections of the semaphore algorithm. A lock must be used to ensure that there is mutex, and there is busy waiting when wait()
and signal()
are called. Semaphores do not completely eliminate busy waiting, however the busy waiting involved with the wait(
) and signal()
functions is way less than the busy waiting involved with using a spinlock.
Applying semaphores to the MPC problem¶
shared vars:
- write index
int in
- read index
int out
char buf[N]
- one binary and one counting semaphore
1 2 3 4 5 6 7 |
|
Producer program:
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 |
|
Conditional Variables¶
Conditional variables can be used to ensure that certain code is executed in order by multiple processes. A process can wait for the completion of a given event on a particular object. It is used to communicate between processes or threads when certain conditions become true
.
An event is a change in the state of some condition or object that a process can listen to. It can wait to be awakened later by a signalling process which actually changes the condition/object.
1 2 |
|
Process 1:
1 2 3 4 5 6 |
|
Process 2:
1 2 3 4 5 6 7 |
|
process 2 automatically reacquires the lock when it is awoken after process 1 sets cond_x = true
and signals process 2. If another process has the mutex lock, process 2 won't be awoken. After being awoken, it will check if the cond_x
is true before proceeding to the CS. This is important because process 1 may not have executed at all in the time that process 2 was sleeping
Conditional Variables allow us to block a process out of the CS based on an arbitrary condition, even when the CS is empty. Mutex locks cannot do this alone.