Summary: Last time
Introduction to concurrency
Threads in Java
The shared update problem: mutex
Today
Specifying atomic actions
Solving the shared update problem
Achieving mutex with shared variables
Introduction to a first programming language construct for synchronisation: semaphores
It is the property that only one process can execute in a given piece of code at any given time
How can we achieve it?
Theory: possible with just shared variables
Practice: programming language features (semaphores, monitors, etc.)
Not part of Java
We use a notation to specify atomic actions (pseudo-code)
<S> – statement S is executed atomically
<await (B) S> – execute <S>, starting only when B is true
await
await
statement is very expressive
Mutual exclusion
Conditional synchronisation
Difficult to implement in general
Though, some special cases are easy
await
statement without body
<await (B) ;>
Sufficient for solving the shared update problem in low-level programming
Other interesting cases will come later
await
statement without body
<await (B) ;>
B
must satisfy at-most-once property (limited-critical-reference)
Critical reference
Assigned in one process and occurs in another, or
Occurs in one process and is assigned in another
At most one critical reference per program statement
while (!B) ;
How could we solve it?
The piece of code that performs counter++
should be atomic
public void enter() { < counter++; > }
Clever programming
Hardware support (multiprocessor systems)
Programming language support
You have not taken the course, so we will try to solve this problem using only instructions from sequential programming!
General overview
process CS ((int i=0;i<N;i++)) {
while (true) {
Non-critical section
Entry protocol
Critical section
Exit protocol
Non-critical section
}
}
Assumptions
No variables are shared between critical and non-critical sections and the protocol
The critical section always terminates
Read/write operations are atomic (x=1
)
Scheduler is weakly fair
A
process waiting to execute <await(B) S>
where B
is constantly
true, will eventually get the processorRequirements
Mutex
Critical section
)No deadlock/livelock
Entry protocol
)Starvation
Entry protocol
)Use a variable turn
to indicate who may enter next
int turn = 0;
process CS ((int i=0;i<2;i++)) {
while (true) {
// Non-critical section
while !(turn == i) ;
// Critical Section
turn = (i+1)%2;
}
}
Mutex?
Deadlock
Starvation
Use a flag
to indicate who has entered
private boolean flag[] = {false, false};
process CS ((int i=0;i<2;i++)) {
other = (i+1)%2;
while (true) {
// Non-critical section
while (flag[other]) ;
flag[i] = true;
// Critical section
flag[i] = false;
}
}
Mutex
while
(flag[other])
and since flag[other]
is false, both threads execute
flag[i] = true;
.Deadlock
Starvation
Use a flag
to indicate who wants to enter
private boolean flag[] = {false, false};
process CS ((int i=0;i<2;i++)) {
other = (i+1)%2;
while (true) {
// Non-critical section
flag[i] = true;
while (flag[other]) ;
// Critical section
flag[i] = false;
}
}
Mutex
Deadlock
flag[i] = true
followed by while (flag[other])
, which make them stuck in the loop.Starvation
flag+turn
: I want to enter, after you
private int turn = 0;
private boolean flag[] = {false, false};
process CS ((int i=0;i<2;i++)) {
other = (i+1)%2;
while (true) {
flag[i] = true;
turn = other
while (flag[other] && turn==i) ;>
// Critical section
flag[i] = false;
}}
It is not easy to show properly
n
) is even worseTesting
Exponentially many traces
A given scheduler (implementation) may only explore a small number of traces
Mathematical proof
Alternative algorithms explored in the book
We only assumed an atomic:
Most modern hardware has larger atomic operations
Used to implement multiprocessor synchronisation at a lower level
Operating systems
Embedded systems
As a pure software solution to the problem
These algorithms are not practical
They all contain a busy-wait loop
while (B) ;
Consumes a great deal of processor resources and is very inefficient
As it is, it does not work in Java! (see more of this later)
But often useful in low-level programming
OS
Embedded devices
The specification of the JVM does not guarantee that Peterson's algorithm will work (Check this post explaining why)
Java memory model
Happens-before relationship
int x, y, z, w; x = 0; y = 5; z = x; w = y;
There are no guarantees regarding updates on shared-variables
Local variables fulfill the happens-before relationship
An stronger relationship is needed with concurrency: synchronizes-with
volatile
keyword An array volatile means that the updates to the references of the elements is visible to other threads, not the update of the elements itself!
In Java, you cannot make arrays with volatile elements (Check this post)
A more suitable solution would be as follows:
Entry protocol: if Critical section is busy then sleep, otherwise enter
Exit protocol: if there are sleeping processes, wake one, otherwise mark the critical section as not busy
Semaphores support this solution
Invented in the mid 60’s
An abstract datatype containing a nonnegative integer accessed by two atomic operations P and V
P stands for passeren which means
”to pass” (in Java, acquire
)
V stands for vrygeven which means
”to release” (in Java, release
)
Dijkstra was Dutch
Specification
class Semaphore { private int value; Semaphore(int init): < value = init > acquire(): < await (value>0) value = value – 1 > release(): < value = value + 1 > }
Java has a library support
java.util.concurrent
Semaphore mutex = new Semaphore(1); public void run() { try { while (true) { //Non-critical section mutex.acquire(); //Critical Section mutex.release(); //Non-critical section }} catch(InterruptedException e) { }}
A semaphore which only ever takes on the values 0 and 1 is called a binary semaphore
When a binary semaphore s
is used for simple mutex:
s.acquire(); //Critical Section s.release();
A lock is created for every object in Java
To use this lock we employ the keyword synchronized
class MutexCounter { private int counter = 0; public synchronized void increment() { counter++; } }
Alternative to a synchronized method is a synchronized block
class MutexCounter { private int counter = 0; public void increment() { // lock this object synchronized (this) { counter++; } }}
Locks
Each object has a lock
Each lock has a queue of waiting threads
The order of the queue is not specified (FIFO, LIFO, etc.)
for(int j = 0; j<5000000; j++) { //System.out.println("Process "+x+" enters "+j); try {mutex.acquire() ;} catch (InterruptedException e) {} enter(); mutex.release() ; } ;
Peterson's algorithm to solve the shared update problem
Introduction to semaphores
Locks