235 lines
7.7 KiB
Markdown
235 lines
7.7 KiB
Markdown
16/10/20
|
|
|
|
## Mutexes
|
|
|
|
**Mutexes** are an approach for mutual exclusion **provided by the operating system** containing a Boolean lock variable to indicate availability.
|
|
|
|
> The lock variable is set to **true** if the lock is available
|
|
>
|
|
> Two atomic functions are used to **manipulate the mutex**
|
|
>
|
|
> 1. `acquire()` - called **before** entering a critical section, Boolean set to **false**
|
|
> 2. `release()` - called **after** exiting the critical section, Boolean set to **true** again.
|
|
|
|
```c
|
|
acquire() {
|
|
while(!available); // busy wait
|
|
available = false;
|
|
}
|
|
```
|
|
|
|
```c
|
|
release() {
|
|
available = true;
|
|
}
|
|
```
|
|
|
|
`acquire()` and `release()` must be **atomic instructions** .
|
|
|
|
* No **interrupts** should occur between reading and setting the lock.
|
|
* If interrupts can occur, the follow sequence could occur.
|
|
|
|
```c
|
|
T_i => lock available
|
|
... T_j => lock available
|
|
... T_j sets lock
|
|
T_i sets lock ...
|
|
```
|
|
|
|
The process/thread that acquires the lock must **release the lock** - in contrast to semaphores.
|
|
|
|
| Pros | Cons |
|
|
| ------------------------------------------------------------ | ------------------------------------------------------------ |
|
|
| Context switches can be **avoided**. | Calls to `acquire()` result in **busy waiting**. Shocking performance on single CPU systems. |
|
|
| Efficient on multi-core systems when locks are **held for a short time**. | A thread can waste it's entire time slice busy waiting. |
|
|
|
|

|
|
|
|
|
|
|
|
## Semaphores
|
|
|
|
> **Semaphores** are an approach for **mutual exclusion** and **process synchronisation** provided by the operating system.
|
|
>
|
|
> * They contain an **integer variable**
|
|
> * We distinguish between **binary** (0-1) and **counting semaphores** (0-N)
|
|
>
|
|
> Two **atomic functions** are used to manipulate semaphores**
|
|
>
|
|
> 1. `wait()` - called when a resource is **acquired** the counter is decremented.
|
|
> 2. `signal()` / `post()` is called when the resource is **released**.
|
|
>
|
|
> **Strictly positive values** indicate that the semaphore is available, negative values indicate the number of processes/threads waiting.
|
|
|
|
```c
|
|
//Definition of a Semaphore
|
|
typedef struct {
|
|
int value;
|
|
struct process * list;
|
|
} semaphore;
|
|
```
|
|
|
|
```c
|
|
wait(semaphore * S) {
|
|
S->value--;
|
|
if(S->value < 0) {
|
|
add process to S->list
|
|
block(); // system call
|
|
}
|
|
}
|
|
```
|
|
|
|
```c
|
|
post(semaphore * S) {
|
|
S->value++;
|
|
if (S->value <= 0) {
|
|
remove a process P from S->list;
|
|
wakeup(P); // system call
|
|
}
|
|
}
|
|
```
|
|
|
|
```
|
|
Thread N
|
|
...
|
|
...
|
|
wait(&s)
|
|
...
|
|
(wakeup)
|
|
...
|
|
post(&s)
|
|
```
|
|
|
|
Calling `wait()` will **block** the process when the internal **counter is negative** (no busy waiting)
|
|
|
|
1. The process **joins the blocked queue**
|
|
2. The **process/thread state** is changed from running to blocked
|
|
3. Control is transferred to the process scheduler.
|
|
|
|
Calling `post()` **removes a process/thread** from the blocked queue if the counter is less than or equal to 0.
|
|
|
|
1. The process/thread state is changed from ***blocked** to **ready**
|
|
2. Different queuing strategies can be employed to **remove** process/threads e.g. FIFO etc
|
|
|
|
The negative value of the semaphore is the **number of processes waiting** for the resource.
|
|
|
|
`block()` and `wait()` are system called provided by the OS.
|
|
|
|
`post()` and `wait()` **must** be **atomic**
|
|
|
|
> Can be achieved through the use of mutexes (or disabling interrupts in a single CPU system)
|
|
>
|
|
> Busy waiting is moved from the **critical section** to `wait()` and `post()` (which are short anyway - the original critical sections themselves are usually much longer)
|
|
|
|
We must lock the variable `value`.
|
|
|
|
```c
|
|
post(semaphore * S) {
|
|
// lock the mutex
|
|
S->value++;
|
|
if (S->value <= 0) {
|
|
remove a process P from S->list;
|
|
wakeup(P); // system call
|
|
}
|
|
//unlock mutex
|
|
}
|
|
```
|
|
|
|
Semaphores put your code to sleep. Mutexes apply busy waiting to user code.
|
|
|
|
Semaphores within the **same process** can be declared as **global variables** of the type `sem_t`
|
|
|
|
> * `sem_init()` - initialises the value of the semaphore.
|
|
> * `sem_wait()` - decrements the value of the semaphore.
|
|
> * `sem_post()` - increments the values of the semaphore.
|
|
|
|

|
|
|
|
Synchronising code does result in a **performance penalty**
|
|
|
|
> * Synchronise only **when necessary**
|
|
>
|
|
> * Synchronise as **few instructions** as possible (synchronising unnecessary instructions will delay others from entering their critical section)
|
|
|
|
```c
|
|
void * calc(void * increments) {
|
|
int i, temp = 0;
|
|
for(i = 0; i < *((int*) increments);i++) {
|
|
temp++;
|
|
}
|
|
sem_wait(&s);
|
|
sum+=temp;
|
|
sem_post(&s);
|
|
}
|
|
```
|
|
|
|
**Figure**: optimised `calc` function
|
|
|
|
#### Starvation
|
|
|
|
> Poorly designed **queueing approaches** (e.g. LIFO) may results in fairness violations
|
|
|
|
#### Deadlock
|
|
|
|
> Two or more processes are **waiting indefinitely** for an event that can be caused only by one of the waiting processes or thread.
|
|
|
|
#### Priority Inversion
|
|
|
|
> Priority inversion happens when a high priority process (`H`) has to wait for a **resource** currently held by a low priority process (`L`)
|
|
>
|
|
> Priority inversion can happen in chains e.g. `H` waits for `L` to release a resource and L is interrupted by a medium priority process `M`.
|
|
>
|
|
> This can be avoided by implementing priority inheritance to boost `L` to the `H`'s priority.
|
|
|
|
|
|
|
|
## The Producer and Consumer Problem
|
|
|
|
> * Producer(s) and consumer(s) share N **buffers** (an array) that are capable of holding **one item each** like a printer queue.
|
|
> * The buffer can be of bounded (size N) or **unbounded size**.
|
|
> * There can be one or multiple consumers and or producers.
|
|
> * The **producer(s)** add items and **goes to sleep** if the buffer is **full** (only for a bounded buffer)
|
|
> * The **consumer(s)** remove items and **goes to sleep** if the buffer is **empty**
|
|
|
|
The simplest version of this problem has **one producer**, **one consumer** and a buffer of **unbounded size**.
|
|
|
|
* A counter (index) variable keeps track of the **number of items in the buffer**.
|
|
* It uses **two binary semaphores:**
|
|
* `sync` **synchronises** access to the **buffer** (counter) which is initialised to 1.
|
|
* `delay_consumer` ensures that the **consumer** goes to **sleep** when there are no items available, initialised to 0.
|
|
|
|

|
|
|
|
It is obvious that any manipulations of count will have to be **synchronised**.
|
|
|
|
**Race conditions** will still exist:
|
|
|
|
> When the consumer has **exhausted the buffer** (when `items == 0`), it should go to sleep but the producer increments `items` before the consumer checks it.
|
|
>
|
|
> * Consumer has removed the **last element**
|
|
> * The producer adds a **new element**
|
|
> * The consumer should have gone to sleep but no longer will
|
|
> * The consumer consumes **non-existing elements**
|
|
>
|
|
> **Solutions**:
|
|
>
|
|
> * Move the consumers' if statement inside the critical section
|
|
|
|
|
|
|
|
### Producers and Consumers Problem
|
|
|
|
The previous code (one consumer one producer) is made to work by **storing the value of** `items`
|
|
|
|
A different variant of the problem has `n` consumers and `m` producers and a fixed buffer size `N`. The solution is based on **3 semaphores**
|
|
|
|
1. `sync` - used to **enforce mutual exclusion** for the buffer
|
|
|
|
2. `empty` - keeps track of the number of empty buffers, initialised to `N`
|
|
|
|
3. `full` - keeps track of the number of **full buffers** initialised to 0.
|
|
|
|
The `empty` and `full` are **counting semaphores** and represent **resources**.
|
|
|
|

|