You are on page 1of 48

Chapter 5: Process

Synchronization
Chapter 5: Process Synchronization
● Background
● The Critical-Section Problem
● Mutex Locks
● Semaphores: what are they and how are they implemented
● Classic Problems of Synchronization
● Synchronization Examples in C and Java

© Dr Yigal Hoffner 17 June 2020 2


Objectives

● To present the concept of process synchronization


● To introduce the critical-section problem, whose solutions can be
used to ensure the consistency of shared data
● To present both software and hardware solutions of the critical-
section problem
● To examine several classical process-synchronization problems
● To explore several tools that are used to solve process
synchronization problems

© Dr Yigal Hoffner 17 June 2020 3


Background
● Processes/threads can execute concurrently
● May be interrupted at any time, partially completing execution before CPU
switches attention to another process
● In a pre-emptive operating system, this could happen at any point in the progress
of a process/thread
● It is not possible to assume anything about the progress of two or more
processes/threads relative to each other
● There are two main types of coordination:
● Keeping the progress of processes/threads in step with each other
● Preventing processes/threads from accessing the same data structure at the same
time
● Concurrent access to shared data may result in data inconsistency – requiring some
form of mutual exclusion
● Maintaining data consistency requires mechanisms to ensure the orderly execution of
cooperating processes

© Dr Yigal Hoffner 17 June 2020 4


An example problem
– requiring inter-process interaction

© Dr Yigal Hoffner 17 June 2020 5


Producer-Consumer Problem
● The producer consumer problem is a synchronization problem that deals with a shared buffer -
where a producer produces items and enters them into the buffer. The consumer removes the items
from the buffer and consumes them
● The code has to ensure that:
● The Producer does not add data when the Buffer is full
● The Consumer does not take data when the Buffer is empty
● A popular paradigm for cooperating processes
● The problem & solutions have several dimensions:
● Buffering
4 Unbounded-buffer places no practical limit on the size of the buffer
4 Bounded-buffer assumes that there is a fixed buffer size
– Single cell buffer
– Cyclical buffer
● Single or multiple producers/consumers
4 Single producer & consumer
4 Multiple producers & consumers (multi-process or multi-threaded environment)
● Blocking or non-blocking operations
● Busy waiting or Suspension waiting (when buffer is empty/full)

Producer buffer Consumer

© Dr Yigal Hoffner 17 June 2020 6


Blocking versus non-blocking operations

No coming back
until successful

Blocking or non-blocking
operations

Blocking Non-Blocking

Suspension
bool put(int value)
Busy
(O/S)

bool indicates
void put(int value) success or failure

On return –
always successful
© Dr Yigal Hoffner 17 June 2020 7
Producer-Consumer: Single Cell buffer solution

● Single producer and consumer


● Bounded buffer with flag = {full, empty};
● Algorithm?

Producer Consumer

While(Flag == full) {}; While(Flag == empty) {};


Buffer = data; myData = Buffer;
Flag = full; Flag = empty;

Flag

Producer Buffer Consumer

© Dr Yigal Hoffner 17 June 2020 8


Producer-Consumer: cyclical-buffer solution
consumer
producer
out
in

● Shared data
#define BUFFER_SIZE 10 BUFFER_SIZE-1 y z
0 1
typedef struct {
2
. . .
buffer
} item;
item buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
Producer Consumer

item next_produced; item next_consumed;


while (true) { while (true) {
while (((in+1) % BUFFER_SIZE) while (in == out) {}; /* wait */
== out) {}; /* wait */
buffer[in] = next_produced; next_consumed = buffer[out];
in = (in + 1) % BUFFER_SIZE; out = (out + 1) % BUFFER_SIZE;
} }
This works well
in a single
producer - single
consumer setup
© Dr Yigal Hoffner 17 June 2020 9
Producer - Consumer: cyclical-buffer solution
● Shared data
#define BUFFER_SIZE 10
typedef struct { Another version:
. . . counter counts the
number of items in
} item; buffer
item buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
int counter = 0;
while (true) {
/* produce an item in next produced */

while (counter == BUFFER_SIZE) {}; /* wait */


buffer[in] = next_produced;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
while (true) {
while (counter == 0) {}; /* wait */
next_consumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
/* consume the item in next consumed */
}

© Dr Yigal Hoffner 17 June 2020 10


Interference among threads
● The previous solution work well for a single producer and a single consumer
● The lines:
● in = (in + 1) % BUFFER_SIZE;
● out = (out + 1) % BUFFER_SIZE;
● Do not interfere with each other and with the empty/full condition test
● What about multiple consumers and multiple producers doing?
● Counter++ & counter--

© Dr Yigal Hoffner 17 June 2020 11


Race Conditions
& Critical Sections

© Dr Yigal Hoffner 17 June 2020 12


Race Condition

High-level constructs such as:


counter++ and counter– are not atomic

Process/Thread Process/Thread
● counter++ could be implemented as ● counter-- could be implemented as

register1 = counter register2 = counter


register1 = register1 + 1 register2 = register2 - 1
counter = register1 counter = register2

● Consider this execution interleaving with “count = 5” initially:


S0: producer execute register1 = counter {register1 = 5}
S1: producer execute register1 = register1 + 1 {register1 = 6}
S2: consumer execute register2 = counter {register2 = 5}
S3: consumer execute register2 = register2 – 1 {register2 = 4}
S4: producer execute counter = register1 {counter = 6 }
S5: consumer execute counter = register2 {counter = 4}

© Dr Yigal Hoffner 17 June 2020 13


Critical Section Problem

● Consider system of n processes {p0, p1, … pn-1}


● Each process has critical section segment of code
● Process may be changing common variables, updating table, writing file, etc.
● When one process in critical section, no other may be in its critical section
● The Critical section problem is to design protocol to solve this
● Each process must ask permission to enter critical section in entry section, may
follow critical section with exit section, then remainder section

© Dr Yigal Hoffner 17 June 2020 14


Critical Section

● General structure of process Pi

Shared
Data
Structure

© Dr Yigal Hoffner 17 June 2020 15


Solution to Critical-Section Problem
⚫ Assume that each process executes at a nonzero speed
⚫ No assumption concerning relative speed of the n processes

1. Mutual Exclusion - If process Pi is executing in its critical section, then no


other processes can be executing in their critical sections Safety

2. Progress - If no process is executing in its critical section and there exist some
processes that wish to enter their critical section, then the selection of the
processes that will enter the critical section next cannot be postponed
indefinitely Liveness
(deadlock free)
3. Bounded Waiting - A bound must exist on the number of times that other
processes are allowed to enter their critical sections after a process has made a
request to enter its critical section and before that request is granted

Liveness
(starvation free
& fairness)

© Dr Yigal Hoffner 17 June 2020 16


Critical-Section Handling in OS

Two approaches depending on whether kernel itself is preemptive or non-


preemptive:
● Preemptive – allows preemption of process when running in kernel mode
● Non-preemptive – runs until exits kernel mode, blocks, or voluntarily
yields CPU
4 Essentially free of race conditions in kernel mode – but only in a uni-
processor system. In a multiprocessor system – having a non-preemptive
operating system does not help with the critical section indivisibility

© Dr Yigal Hoffner 17 June 2020 17


Synchronization Hardware

● Many systems provide hardware support for implementing the critical section code.
● All solutions below based on idea of locking
● Protecting critical regions via locks
● Uniprocessors – could disable interrupts
● Currently running code would execute without preemption
● Does not work on multiprocessor systems
● Modern machines provide special atomic hardware instructions
4 Atomic = non-interruptible
● Either test memory word and set value
● Or swap contents of two memory words

© Dr Yigal Hoffner 17 June 2020 18


Solution to Critical-section Problem Using Locks

do {
acquire lock
critical section
release lock
remainder section
} while (TRUE);

● The concept of a "lock" has many different implementations:


● Spinning - spinlock or busy-waiting locks
● Suspending or blocking locks
● "The idea of "locking" appears in several mechanisms: Requires
interaction
● Locks for multi-threaded coordination with OS
● Semaphores, Mutex
● Condition variables

© Dr Yigal Hoffner 17 June 2020 19


Mutex
● Previous solutions are complicated and generally inaccessible to application
programmers
● OS designers build software tools to solve critical section problem
● Simplest is mutex
● Protect a critical section by first acquire() a lock then release() the
lock
● Shared Boolean variable indicating if lock is available or not
● Calls to acquire() and release() must be atomic
● Usually implemented via hardware atomic instructions
● This solution can be implemented by
● busy waiting as a spinlock OR by
● blocking (and signaling)

A mutex helps multiple tasks serialize their accesses to shared


resources and gives the waiting tasks a place to wait for their turn.
In that sense – it is very much like a blocking lock

© Dr Yigal Hoffner 17 June 2020 20


The get_and_set() indivisible instruction
Read-Modify-Write

(RMW) operation
● Semantics of the get_and_set() instruction: semantics
AtomicBoolean state = false;
boolean get_and_set(boolean target)
{ Atomic operation
boolean rv = state; – implemented by
state = target; hardware
return rv:
}
● Executed atomically

● Use the ●above


do {instruction to create the lock below (see next page)
acquire lock
The get_and_set()
critical section
instruction is often referred
release lock to as
remainder section test_and_set() in literature.
} while (true);
However, there is no test in
this instruction.

© Dr Yigal Hoffner 17 June 2020 21


Spinning implementation of lock acquire() and
release()
Spinning implementation of acquire() and release() with get_and_set()

1. Returns the original value of the memory location on which the operation is
carried out
2. Shared Boolean variable lock, initialized to “FALSE” 🡺 lock is free
3. Set the new value of passed parameter to “TRUE” 🡺 try to lock

● The implementation of the lock:

  AtomicBoolean state = false;  // initialisation


● acquire() {
while (get_and_set(true)) {}; /* wait */
}

● release() {
state = false; // release lock
}
Spin Locks in a uni processor systems are considered bad pratice.
© Dr Yigal Hoffner 17 June 2020 22
Blocking implementation of acquire() and release()
● Another option is to use operating system facilities in order to block the process
asking to acquire the lock, in case the lock has already been acquired by another
process
● The process is blocked until the process which holds the lock releases the it by
doing a release()
● This requires the ability to block a process/thread when doing acquire() and wake
it up later when another process/thread executes the release() operation
● Locks which provide a combination of spinning and then suspending/blocking
after a certain interval are also available

© Dr Yigal Hoffner 17 June 2020 23


Blocking Mutex operations
int pthread_mutex_lock (pthread_mutex_t *m);
int pthread_mutex_unlock (pthread_mutex_t *m);
int pthread_mutex_trylock (pthread_mutex_t *m); // same as  pthread_mutex_trylock(), except
that if the mutex object referenced by mutex is currently locked (by any thread, including the current thread),
the call shall return immediately
int pthread_mutex_destroy (pthread_mutex_t *m);

● Return Value
● If successful, the pthread_mutex_lock() and pthread_mutex_unlock() functions
shall return zero; otherwise, an error number shall be returned to indicate the
error.
● The pthread_mutex_trylock() function shall return zero if a lock on the mutex
object referenced by mutex is acquired. Otherwise, an error number is returned to
indicate the error.

© Dr Yigal Hoffner 17 June 2020 24


SEMAPHORES

© Dr Yigal Hoffner 17 June 2020 25


Semaphore & Mutex in different languages
● We use wait() and signal() as the generic – language independent notation
- for the 2 main operations on a Semaphore or Mutex

● There are different names for basically the same operations of Semaphores in
different languages:
● POSIX  (C, C++)
4 Semaphore: sem_wait() sem_post()
4 Mutex: pthread_mutex_lock()   pthread_mutex_unlock()
4 Spin Lock: pthread_spin_lock()    pthread_spin_unlock()  
● C#
4 Semaphore: waitOne() release() 
● Java
4 Semaphore: acquire() release()    
4 Java locks: lock() unlock()
(locks are similar in some ways to semaphores but also have some differences)    

© Dr Yigal Hoffner 17 June 2020 26


Semaphore concept
● A semaphore is a synchronization mechanism that can be used to provide mutual
exclusion and conditional synchronization
● A semaphore is implemented as an integer variable with 2 operations on it: wait &
signal
● Generic definition of semaphores
● wait() or down()
● Atomic action: Wait for semaphore value to become > 0, then decrement it

● signal() or up()
● Atomic action: Increment semaphore value by 1

● Can be implemented with


● Busy-wait or spinning
● Blocking and unblocking a process

© Dr Yigal Hoffner 17 June 2020 27


Using Semaphore for Synchronisation
● Counting semaphore – integer value can range over an unrestricted domain
● Binary semaphore – integer value can range only between 0 and 1
● Same as a mutex lock
● Can solve various synchronization problems
● Consider P0 and P1 that require S1 to happen before S2 i.e. (S1 → S2 )

Semaphore sync =0;


P0 P1
tim
e
S1; OS freezes
Wait(sy
nc) P1
until a
Signal(s signal
ync) comes
from P0

S2;
/* Task P0 */ /* Task P1 */
S1; wait(sync); // Wait for
signal(sync); // Send the signal signal
S2;
© Dr Yigal Hoffner 17 June 2020 28
Semaphore implemented by OS freeze (blocking)
● A semaphore is a synchronization mechanism – implemented as an integer variable with 2 operations on it:
wait & signal
● The semaphore is an OS data structure and the wait & signal methods are part of the kernel
● Semaphores are used to control access to a common resource by multiple processes in a concurrent system
● A semaphore is a structure with 2 fields
● The two operations on a semaphore are (Dijkstra’s definition): wait() and signal()
● Must guarantee that no two processes can execute the wait()and signal() on same semaphore at the
same time – indivisibility of wait() and signal()

struct semaphore {
int value; // semaphore (counter) value;
struct process *queue // a queue of blocked processes
};

wait(struct sempahore *s) { signal(struct semaphore *s) {


s->value--; s->value++;

indivisible
indivisible

IF (s->value < 0) IF (s->value <= 0)


THEN add this process to S->list; THEN remove a process P from S-list;
block(); wakeup(P);
FI FI
}
© Dr Yigal Hoffner 17 June 2020
}
29
Indivisibility/atomicity
● To make wait() and signal() indivisible is easy when executed in a kernel that
blocks interrupts while in Kernel Mode
● This solution does not work in a multicore/processor system
● In fact, we also need a bit of spin-locking in order to implement blocking-locks
● So the wait() and signal() use a spin lock which can be implemented with the help
of a read-modify-write (RMW) operations such as:

● It is OK because wait() and signal() are relatively short sections of code

© Dr Yigal Hoffner 17 June 2020 30


Semaphore Implementation (with no Busy waiting)
● Semaphores are implemented in the Kernel
● Called through a System Call from user code
● When called from inside the Kernel – it is called directly (not as a System Call)
● With each semaphore there is an associated waiting queue
● Each entry in a waiting queue has two data items:
● value (of type integer)
● pointer to next record in the list
● Two operations:
● block – place the process invoking the operation on the appropriate waiting queue
● wakeup – remove one of processes in the waiting queue and place it in the ready queue

© Dr Yigal Hoffner 17 June 2020 31


Advantages & Disadvantages of Semaphores
● Advantages of Semaphores
● Semaphores (used as Mutex) allow only one process into the critical section. They
follow the mutual exclusion principle strictly
● There is no resource wastage because of busy waiting in semaphores as processor time
is not wasted unnecessarily to check if a condition is fulfilled to allow a process to
access the critical section

● Disadvantages of Semaphores
● Semaphores are complicated to use so the wait and signal operations must be
implemented in the correct order to prevent deadlocks
● Semaphores are impractical for large scale use as their use leads to loss of modularity.
This happens because the wait and signal operations prevent the creation of a structured
layout for the system
● Semaphores may lead to a priority inversion where low priority processes may access
the critical section first and high priority processes later

© Dr Yigal Hoffner 17 June 2020 32


IMPLEMENTATION OF
A SEMAPHORE IN POSIX

© Dr Yigal Hoffner 17 June 2020 33


Sharing named POSIX semaphores
● Choose a name for your semaphore
#define SNAME "/mysem“
● sem_open() creates a new POSIX semaphore or opens an existing semaphore. The semaphore
is identified by name.

1. Use sem_open with O_CREAT in the process that creates them


sem_t *sem = sem_open(SNAME, O_CREAT, 0644, 3); /* Initial value is 3. */

2. Open a pre-existing semaphore in the other processes


sem_t *sem = sem_open(SNAME, 0); /* Open a pre-existing semaphore. */

© Dr Yigal Hoffner 17 June 2020 34


Implementation of a Semaphore
● Semaphores are implemented in the Kernel
● Called through a System Call from user code
● When called from inside the Kernel – it is called directly (not as a System Call)

sem_t *sem = sem_open(“sema”, O_CREAT, 0644, 3); sem_t *sem = sem_open (“sema”, 0);

Producer Consumer

P0 P1
sem_open() sem_open()

SysCall SysCall User Mode


Kernel Mode
sema

Semaphore Q: 1
signal(struct
semaphore *s) { }
Kernel wait(struct
Data sempahore *s) { }
© Dr Yigal Hoffner 17 June 2020 35
Problems with Semaphores

● Incorrect use of semaphore operations:


● Wrong order of wait-signal couples:

4 signal (mutex) …. wait (mutex)

4 wait (mutex) … wait (mutex)

● Omitting of wait (mutex) or signal (mutex) (or both)

● Deadlock and starvation are possible – see next page

© Dr Yigal Hoffner 17 June 2020 36


Deadlock and Starvation
● Deadlock – two or more processes are waiting indefinitely for an event that
can be caused by only one of the waiting processes
● Let S and Q be two semaphores initialized to 1
P0 P1
wait(S); wait(Q);
wait(Q); wait(S);
... ...
signal(S); signal(Q);
signal(Q); signal(S);

● Starvation – indefinite blocking


● A process may never be removed from the semaphore queue in which it is
suspended
● Priority Inversion – Scheduling problem when lower-priority process holds
a lock needed by higher-priority process
● Solved via priority-inheritance protocol

© Dr Yigal Hoffner 17 June 2020 37


Classical Problems of
Synchronization

● Classical problems using semaphores


● Bounded-Buffer Problem
● Readers and Writers Problem
● Dining-Philosophers Problem

© Dr Yigal Hoffner 17 June 2020 38


Bounded-Buffer Problem

● A buffer with n spaces, each can hold one item


● Semaphore mutex initialized to the value 1
● Semaphore full initialized to the value 0
● Semaphore empty initialized to the value n

Producer buffer Consumer

© Dr Yigal Hoffner 17 June 2020 39


Single Producer – single Consumer: Single Cell Buffer Problem
Shared memory:
Data buffer;
Semaphore full = 0 // Initially, full slots are 0
Semaphore empty = 1 // slots is empty initially
Producer Consumer
DO forever { DO forever {

wait(empty); wait(full);
buffer = data; data = buffer;
signal(full); signal(empty);
} }

© Dr Yigal Hoffner 17 June 2020 40


Single Producer & Consumer: Bounded Buffer Problem
Shared memory:
Data buffer;
Semaphore full = 0 // Initially, all slots are empty. Thus full slots are 0
Semaphore empty = n // All slots are empty initially

Producer Consumer
do { // produce an item
do { // consumes item
wait(empty); wait(full);

// place in buffer // remove item from buffer

signal(full); signal(empty);
} while (true)
} while (true)

wait(struct sempahore *s) { signal(struct semaphore *s) {


s->value--; s->value++;
IF (s->value < 0) IF (s->value <= 0)
THEN add this process to S->list; THEN remove a process P from S-list;
block(); wakeup(P);
FI FI
}
© Dr Yigal Hoffner 17 June 2020
}
41
multiple Producers & Consumers: with 3 semaphores IN user space
Shared memory:
Data buffer;
Semaphore full = 0 // Initially, full slots are 0
Semaphore empty = n // All slots are empty initially
Semaphore mutex = 1 // mutual exclusion
Producer Consumer
do { do {
//produce an item

wait(empty); wait(full);
wait(mutex); wait(mutex);

//place item in buffer // remove item from buffer

signal(mutex); signal(mutex);
signal(full); signal(empty);

// consumes item
} while (true) } while (true)

wait(struct sempahore *s) { signal(struct semaphore *s) {


s->value--; s->value++;
IF (s->value < 0) IF (s->value <= 0)
THEN add this process to S->list; THEN remove a process P from S-list;
block(); wakeup(P);
FI FI
}
© Dr Yigal Hoffner 17 June 2020
}
42
Readers-Writers Problem
● A data set is shared among a number of concurrent processes
● Readers – only read the data set; they do not perform any updates
● Writers – can both read and write
● Problem – allow multiple readers to read at the same time
● Only one single writer can access the shared data at the same time
● Several variations of how readers and writers are considered – all involve
some form of priorities
● Shared Data
● Data set
● Semaphore rw_mutex initialized to 1
● Semaphore mutex initialized to 1
● Integer read_count initialized to 0

© Dr Yigal Hoffner 17 June 2020 43


Readers-Writers Problem with Semaphoes

Semaphore rw_mutex initialized to 1


Semaphore mutex initialized to 1
Integer read_count initialized to 0

● The structure of a reader process ● The structure of a writer process


do {
wait(mutex); do {
read_count++; wait(rw_mutex);
if (read_count == 1) wait(rw_mutex); ...
signal(mutex); /* writing is performed */
... ...
/* reading is performed */ signal(rw_mutex);
... } while (true);
wait(mutex);
read_count--;
if (read_count==0) signal(rw_mutex);
signal(mutex);
} while (true);
Neither sides have priority, but after first reader gets in – if more readers arrive – writer can starve.
So this solution offers no fairness.
When a writer gets in, the first reader that comes is blocked and so will any arriving writers.
Its up to the scheduler whether it will wake up the single reader when the current writer is finished.
So potentially, the readers may get an unfair treatment.

© Dr Yigal Hoffner 17 June 2020 44


Readers-Writers Problem Variations
● First variation – no reader kept waiting unless writer has permission to use
shared object
● Second variation – once writer is ready, it performs the write ASAP
● Both may have starvation leading to even more variations
● Problem is solved on some systems by kernel providing reader-writer locks

© Dr Yigal Hoffner 17 June 2020 45


Dining-Philosophers Problem

● Philosophers spend their lives alternating thinking and eating


● Don’t interact with their neighbors, occasionally try to pick up 2 chopsticks (one at a
time) to eat from bowl
● Need both to eat, then release both when done
● In the case of 5 philosophers
● Shared data
4 Bowl of rice (data set)
4 Semaphore chopstick [5] initialized to 1

© Dr Yigal Hoffner 17 June 2020 46


Dining-Philosophers Problem Algorithm
● The structure of Philosopher i:
do {
wait (chopstick[i] );
wait (chopStick[ (i + 1) % 5] );

// eat

signal (chopstick[i] );
signal (chopstick[ (i + 1) % 5] );

// think

} while (TRUE);
● What is the problem with this algorithm?

© Dr Yigal Hoffner 17 June 2020 47


Dining-Philosophers Problem Algorithm (Cont.)

● Deadlock handling
● Allow at most 4 philosophers to be sitting simultaneously at the table
● Allow a philosopher to pick up the forks only if both are available
(picking must be done in a critical section
● Use an asymmetric solution -- an odd-numbered philosopher picks up
first the left chopstick and then the right chopstick. Even-numbered
philosopher picks up first the right chopstick and then the left chopstick

© Dr Yigal Hoffner 17 June 2020 48

You might also like