COMP3891

Concurrency and Synchronisation

Note: most of the points below are applicable to both threads AND processes. For brevity, only threads will be used.

Overview of the Problem

Situations where a result depends on the order of which threads run are known as a race condition.

int count = 0

void increment() {
    int t = count;
    t++;
    count = t;
}

void decrement() {
    int t = count;
    t--;
    count = t;
}

Can produce one of -1, 0, 1 depending on exact execution sequence.

Issue arises when a thread uses a global variable while a different thread is changing it.

Critical Regions

Overview of Critical Regions

Critical regions are sections of the program where shared memory is accessed. Key to solving concurrency problems is to ensure no two threads are in the critical region at the same time.

Requirements for Concurrency Solution

Potential Mutual Exclusion Solutions

Lock variable

Enter when lock == 0, wait otherwise.

Problem: consider the following execution sequence on two threads:

while (TRUE) {
                            while (TRUE) {
                                while (lock != 0);
    while (lock != 0);
    lock = 1;
                                lock = 1;
                                critical();
    critical();
    lock = 0;
    non_critical();
                                lock = 0;
                                non_critical();
                            }
}

Here, thread 2 sees critical region is available and moves on. But before setting the lock, thread 1 runs and also sees critical region is available. Both threads enter the critical region at the same time.

Problem arises because the operation of reading the lock is separate to the operation of setting the lock; this is not an atomic operation.

Taking Turns

while (TRUE) {
    while (turn != 0);
    critical_region();
    turn = 1;
    noncritical_region();
}

while (TRUE) {
    while (turn != 1);
    critical_region();
    turn = 0;
    noncritical_region();
}

Solution works due to strict alternation, but has following problems:

Disabling Interrupts

Interrupts can be disabled before entering a critical region, and reenabled after leaving.

Pros:

Cons:

Hardware Support

Test and set instruction is used to implement lock variables correct. Ensures process of reading and acquiring the lock happens in a single atomic operation.

Pros:

Cons:

Overcoming Busy Waiting Problem

Busy Waiting Problem

Busy waiting is when a thread sits in a tight loop waiting for entry into a critical region. This approach has the problems:

Sleep/Wakeup Idea

Semaphores

Overview

Implementation

typedef struct {
    int count;
    struct process *L;
} semaphore;

Assume two simple operations:

We now define the atomic sempahore operations as such:

wait(S) {
    S.count--;
    if (S.count < 0) {
        // add this process to S.L
        sleep;
    }
}

signal(S) {
    S.count++;
    if (S.count <= 0) {
        // remove a process P from S.L
        wakeup(P);
    }
}

Producer-Consumer (Bounded Buffer) Problem

Poducer produces data items and stores the items in a buffer. Consumer takes the items out of the buffer and consumes them.

We must keep current count of items in the buffer and:

Solving the Producer-Consumer Problem With Semaphores

#define N = 4

sempahore mutex = 1;

semaphore empty = N;

semaphore full = 0;

prod() {                          cons() {
    while (TRUE) {                    while (TRUE) {
        item = produce();                 wait(full);
        wait(empty);                      wait(mutex);
        wait(mutex);                      remove_item();
        insert_item();                    signal(mutex);
        signal(mutex);                    signal(empty);
        signal(full);
    }                                 }
}                                 }

Monitors

Overview

Condition Variables

Overview

Operations