Learning Outcomes

• Understand concurrency is an issue in operating systems and multithreaded applications Concurrency and • Know the concept of a critical region . • Understand how of critical Synchronisation regions can be used to solve concurrency issues – Including how mutual exclusion can be implemented correctly and efficiently. • Be able to identify and solve a producer consumer bounded buffer problem. • Understand and apply standard synchronisation primitives to solve synchronisation problems.

1 2

Making Single-Threaded Code Multithreaded Textbook

• Sections 2.3 & 2.4

Conflicts between threads over the use of a 3 global variable 4

Inter - and Communication Critical Region

• We can control access to the shared resource by controlling access to the code that accesses the resource. ⇒ A critical region is a region of code where We have a shared resources are accessed. – Variables, memory, files, etc… • Uncoordinated entry to the critical region results in a race condition Two processes want to access at same ⇒ Incorrect behaviour, , lost work,… time 5 6

1 Critical Regions Example critical sections

struct node { void insert(struct *item) int data; { struct node *next; item->next = head; }; head = item; struct node *head; }

void init(void) struct node *remove(void) { { head = NULL; struct node *t; } t = head; if (t != NULL) { head = head->next; • Simple last-in-first-out queue } implemented as a linked list. return t; }

Mutual exclusion using critical regions 7 8

Example critical sections Critical Regions struct node { void insert(struct *item) int data; { Also called critical sections struct node *next; item->next = head; }; head = item; struct node *head; } Conditions required of any solution to the critical void init(void) struct node *remove(void) region problem { {  Mutual Exclusion: head = NULL; struct node *t;  No two processes simultaneously in critical region } t = head;  if (t != NULL) { No assumptions made about speeds or numbers of head = head->next; CPUs • Critical sections }  return t; Progress  } No process running outside its critical region may block another process  Bounded  No process must wait forever to enter its critical region 9 10

A solution? A solution?

• A variable while(TRUE) { while(TRUE) { while(lock == 1); while(lock == 1); – If lock == 1, lock = 1; lock = 1; • somebody is in the critical section and we must critical(); critical(); wait lock = 0 lock = 0 – If lock == 0, non_critical(); non_critical(); } } • nobody is in the critical section and we are free to enter

11 12

2 A problematic execution Observation sequence while(TRUE) { • Unfortunately, it is usually easier to show while(TRUE) { while(lock == 1); something does not work, than it is to while(lock == 1); prove that it does work. lock = 1; – Ideally, we’d like to prove, or at least lock = 1; critical(); critical(); informally demonstrate, that our solutions lock = 0 work. non_critical(); } lock = 0 non_critical(); }

13 14

Mutual Exclusion by Taking Turns Mutual Exclusion by Taking Turns

• Works due to strict alternation – Each process takes turns • Cons – Busy waiting – Process must wait its turn even while the other process is doing something else. • With many processes, must wait for everyone to have a turn – Does not guarantee progress if a process no longer needs a turn. • Poor solution when processes require the critical section at Proposed solution to critical region problem differing rates (a) Process 0. (b) Process 1. 15 16

Mutual Exclusion by Disabling Peterson’s Solution Interrupts • See the textbook • Before entering a critical region, disable interrupts • After leaving the critical region, enable interrupts • Pros – simple • Cons – Only available in the kernel – Blocks everybody else, even with no contention • Slows interrupt response time – Does not work on a multiprocessor

17 18

3 Hardware Support for mutual Mutual Exclusion with Test-and-Set exclusion • Test and set instruction – Can be used to implement lock variables correctly • It loads the value of the lock • If lock == 0, – set the lock to 1 – return the result 0 – we acquire the lock • If lock == 1 – return 1 – another thread/process has the lock – Hardware guarantees that the instruction executes atomically. • Atomically: As an indivisible unit. Entering and leaving a critical region using the

19 TSL instruction 20

Test-and-Set Tackling the Busy-Wait Problem • Pros – Simple (easy to show it’s correct) • Sleep / Wakeup – Available at user-level – The idea • To any number of processors • To implement any number of lock variables • When process is waiting for an event, it calls sleep to block, instead of busy waiting. • Cons • The the event happens, the event generator – Busy waits (also termed a spin lock) (another process) calls wakeup to unblock the • Consumes CPU • Livelock in the presence of priorities sleeping process. – If a low priority process has the lock and a high priority process attempts to get it, the high priority process will busy-wait forever. • Starvation is possible when a process leaves its critical section and more than one process is waiting. 21 22

The Producer-Consumer Issues Problem • We must keep an accurate count of items in buffer • Also called the bounded buffer problem – Producer • can sleep when the buffer is full, • A producer produces data items and stores the • and wakeup when there is empty space in the buffer items in a buffer – The consumer can call wakeup when it consumes the first entry of the full buffer • A consumer takes the items out of the buffer and – Consumer consumes them. • Can sleep when the buffer is empty Producer • And wake up when there are items available – Producer can call wakeup when it adds the first item to the buffer

X X X Producer

Consumer X X X

23 Consumer 24

4 Pseudo-code for producer and Problems consumer int count = 0; con() { int count = 0; con() { #define N 4 /* buf size */ while(TRUE) { #define N 4 /* buf size */ while(TRUE) { prod() { if (count == 0) prod() { if (count == 0) while(TRUE) { sleep(); while(TRUE) { sleep(); item = produce() remove_item(); item = produce() remove_item(); if (count == N) count--; if (count == N) count--; sleep(); if (count == N-1) sleep(); if (count == N-1) insert_item(); wakeup(prod); insert_item(); wakeup(prod); count++; } count++; } if (count == 1) } if (count == 1) } Concurrent wakeup(con); wakeup(con); uncontrolled access to the } } buffer } }

25 26

Problems Proposed Solution int count = 0; con() { #define N 4 /* buf size */ while(TRUE) { • Lets use a locking primitive based on test- prod() { if (count == 0) and-set to protect the concurrent access while(TRUE) { sleep(); item = produce() remove_item(); if (count == N) count--; sleep(); if (count == N-1) insert_item(); wakeup(prod); count++; } if (count == 1) } Concurrent wakeup(con); uncontrolled access to the } counter }

27 28

Problematic execution sequence Proposed solution? con() { while(TRUE) { if (count == 0) int count = 0; con() { prod() { #define N 4 /* buf size */ while(TRUE) { while(TRUE) { item = produce() prod() { if (count == 0) while(TRUE) { if (count == N) wakeup without a sleep(); item = produce() sleep(); matching sleep is acquire_lock() acquire_lock() if (count == N) lost remove_item(); insert_item(); sleep(); count++; acquire_lock() count--; release_lock() insert_item(); release_lock(); if (count == 1) count++; if (count == N-1) wakeup(con); release_lock() sleep(); wakeup(prod); acquire_lock() if (count == 1) } remove_item(); wakeup(con); } count--; } release_lock(); } if (count == N-1) wakeup(prod); 29 } 30 }

5 Problem Semaphores

• The test for some condition and actually • Dijkstra (1965) introduced two primitives going to sleep needs to be atomic that are more powerful than simple sleep and wakeup alone. • The following does not work – P(): proberen, from Dutch to test. acquire_lock() if (count == N) – V(): verhogen, from Dutch to increment. sleep(); release_lock() – Also called wait & , down & up.

The lock is held while asleep ⇒ count will never change

31 32

How do they work Implementation

• If a resource is not available, the corresponding • Define a semaphore as a record semaphore blocks any process wait ing for the resource typedef struct { • Blocked processes are put into a process queue int count; maintained by the semaphore (avoids busy waiting!) struct process *L; • When a process releases a resource, it signal s this by } semaphore; means of the semaphore • Signalling resumes a blocked process if there is any • Assume two simple operations: • Wait and signal operations cannot be interrupted – sleep suspends the process that invokes it. • Complex coordination can be implemented by multiple – wakeup( P) resumes the execution of a blocked semaphores process P.

33 34

• Semaphore operations now defined as wait (S): Semaphore as a General S.count--; if (S.count < 0) { Synchronization Tool add this process to S.L; sleep; • Execute B in Pj only after A executed in Pi } • Use semaphore count initialized to 0

signal (S): • Code: S.count++; Pi Pj if (S.count <= 0) { MM remove a process P from S.L; wakeup(P); A wait (flag ) } • Each primitive is atomic signal (flag ) B

35 36

6 Semaphore Implementation of a Solving the producer-consumer Mutex problem with semaphores • Mutex is short for Mutual Exclusion #define N = 4 – Can also be called a lock semaphore mutex = 1; semaphore mutex; mutex.count = 1; /* initialise mutex */ /* count empty slots */ wait(mutex); /* enter the critcal region */ semaphore empty = N;

Blahblah(); /* count full slots */ semaphore full = 0; signal(mutex); /* exit the critical region */ Notice that the initial count determines how many waits can progress before and requiring a signal ⇒ mutex.count initialised as 1 37 38

Solving the producer-consumer problem with semaphores Summarising Semaphores prod() { con() { while(TRUE) { while(TRUE) { • Semaphores can be used to solve a item = produce() wait(full); variety of concurrency problems wait(empty); wait(mutex); wait(mutex) remove_item(); • However, programming with then can be insert_item(); signal(mutex); error-prone signal(mutex); signal(empty); signal(full); } – E.g. must signal for every wait for mutexes } } • Too many, or too few signals or waits, or signals } and waits in the wrong order, can have catastrophic results

39 40

Monitors Monitor • To ease concurrent programming, Hoare (1974) • When a thread proposed monitors. calls a monitor – A higher level synchronisation primitive procedure that – Programming language construct has a thread already inside, it • Idea is queued and it – A set of procedures, variables, data types are sleeps until the grouped in a special kind of module, a monitor. current thread • Variables and data types only accessed from within the exits the monitor. monitor – Only one process/thread can be in the monitor at any one time • Mutual exclusion is implemented by the (which should be less error prone)

41 42

7 Monitors Simple example

monitor counter { Note: “paper” language int count; • Compiler guarantees procedure inc() { only one thread can count = count + 1; be active in the } procedure dec() { monitor at any one count = count –1; time } • Easy to see this } provides mutual exclusion – No race condition on count . Example of a monitor 43 44

How do we block waiting for an Condition Variable event? • To allow a process to wait within the monitor, a condition • We need a mechanism to block waiting for variable must be declared, as an event (in addition to ensuring mutual condition x, y; exclusion) • Condition variable can only be used with the operations wait and signal . – e.g., for producer consumer problem when – The operation buffer is empty or full x.wait(); means that the process invoking this operation is suspended until • Condition Variables another process invokes x.signal(); – The x.signal operation resumes exactly one suspended process. If no process is suspended, then the signal operation has no effect.

45 46

Monitors Condition Variables

• Outline of producer-consumer problem with monitors 47 – only one monitor procedure active at one time 48 – buffer has N slots

8 OS/161 Provided Synchronisation Locks Primitives • Locks • Functions to create and destroy locks struct lock *lock_create(const char *name); • Semaphores void lock_destroy(struct lock *);

• Condition Variables • Functions to acquire and release them

void lock_acquire(struct lock *); void lock_release(struct lock *);

49 50

Example use of locks Semaphores int count; procedure inc() { struct lock *count_lock lock_acquire(count_lock); struct semaphore *sem_create(const char *name, int initial_count); count = count + 1; void sem_destroy(struct semaphore *); main() { lock_release(count_lock); count = 0; } void P(struct semaphore *); count_lock = procedure dec() { void V(struct semaphore *); lock_create(“count lock_acquire(count_lock); lock”); count = count –1; if (count_lock == NULL) lock_release(count_lock); panic(“I’m dead”); } stuff(); }

51 52

Example use of Semaphores Condition Variables int count; procedure inc() { struct cv *cv_create(const char *name); void cv_destroy(struct cv *); struct semaphore P(count_mutex); *count_mutex; count = count + 1; void cv_wait(struct cv *cv, struct lock *lock); V(count_mutex); – Releases the lock and blocks main() { } – Upon resumption, it re-acquires the lock • Note: we must recheck the condition we slept on count = 0; procedure dec() { count_mutex = P(count_mutex); void cv_signal(struct cv *cv, struct lock *lock); sem_create(“count”, count = count –1; void cv_broadcast(struct cv *cv, struct lock *lock); 1); V(count_mutex); – Wakes one/all, does not release the lock – First “waiter” scheduled after signaller releases the lock will re- if (count_mutex == NULL) } panic(“I’m dead”); acquire the lock stuff(); } Note: All three variants must hold the lock passed in.

53 54

9 Condition Variables and Bounded A Producer-Consumer Solution Buffers Using OS/161 CVs int count = 0; Non-solution Solution #define N 4 /* buf size */ lock_acquire(c_lock) lock_acquire(c_lock) prod() { con() { while(TRUE) { while(TRUE) { while (count == 0) if (count == 0) item = produce() lock_acquire(l) cv_wait(c_cv, c_lock); lock_aquire(l) while (count == 0) sleep(); while (count == N) cv_wait(e,l); remove_item(); cv_wait(f,l); item = remove_item(); remove_item(); count--; insert_item(item); count--; count++; if (count == N-1) count--; lock_release(c_lock); if (count == 1) cv_signal(f,l); lock_release(c_lock); cv_signal(e,l); lock_release(l); lock_release() consume(item); } } } }

55 56

Dining Philosophers Dining Philosophers

• Philosophers eat/think • Eating needs 2 forks • Pick one fork at a time • How to prevent deadlock

57 58 Solution to dining philosophers problem (part 1)

Dining Philosophers Dining Philosophers

A non solution to the dining philosophers problem 59 60 Solution to dining philosophers problem (part 2)

10 The Readers and Writers Problem The Readers and Writers Problem

• Models access to a database • E.g. airline reservation system – Can have more than one concurrent reader • To check schedules and reservations – Writers must have exclusive access • To book a ticket or update a schedule

61 A solution to the readers and writers problem 62

The The Sleeping Barber Problem

See the textbook

63 64 Solution to sleeping barber problem.

11