Synchonization

1. Critical Sections & Mutual Exclusion

A critical section is a section of code in which a process (or thread) accesses a shared resource.

Mutual exclusion ensures only a single process can execute a critical section at a time. Processes must request permission to enter a critical section. The requirements are:

1.1 Disabling Interrupts

One way to achieve mutual exclusion is to disable interrupts:

1void extract(int acc_no, int sum) { 2 enum intr_level old_level = intr_disable(); 3 int b = acc[acc_no]; 4 acc[acc_no] = b - sum; 5 intr_set_level(old_level); 6}

This only works on a single-core system. Misbehaving / buggy processes may never release CPU. This is usually only used by kernel code.

1.2 Strict Alternation

Another way to achieve mutual exclusion is to use strict alternation:

1int turn = 0; 2 3void P0() { 4 while (true) { 5 while (turn != 0); 6 critical_section(); 7 turn = 1; 8 noncritical_section(); 9 } 10} 11 12void P1() { 13 while (true) { 14 while (turn != 1); 15 critical_section(); 16 turn = 0; 17 noncritical_section(); 18 } 19}

If P0 takes a long time in the noncritical_section() and P1 is ready to run, P1 will busy-wait until P0 finishes. This violates the second requirement of mutual exclusion. Additionally, one of the processes will not be able to execute its loop twice in a row.

Strict alteration requires busy waiting:

1.3 Peterson's Algorithm

1int turn = 0; 2int interested[2] = {0, 0}; 3 4void enter_critical (int thread) { 5 int other = 1 - thread; 6 interested[thread] = 1; 7 turn = other; 8 while (turn == other && interested[other]); 9} 10 11void leave_critical (int thread) { 12 interested[thread] = 0; 13} 14 15void P0() { 16 enter_critical(0); 17 critical_section(); 18 leave_critical(0); 19} 20 21void P1() { 22 enter_critical(1); 23 critical_section(); 24 leave_critical(1); 25}

1.4 Spin Locks

Atomic instruction provided by most CPUs called TSL. Automatically sets memory location LOCK to 1 and returns the previous value. This can be used to implement a lock:

1void lock (int L) { 2 while (TSL(L) != 0); 3} 4 5void unlock (int L) { 6 L = 0; 7}

Spin locks are locks that require a busy wait. These waste CPU time, and may run into a priority inversion problem:

1.5 Lock Granularity

Lock granularity allows specifying the size of the critical section. For example:

1void extract(int acc_no, int sum) { 2 lock(acc[acc_no]); 3 int b = acc[acc_no]; 4 acc[acc_no] = b - sum; 5 unlock(acc[acc_no]); 6}

Lock overhead - the cost associated with acquiring and releasing locks. Takes into account:

Lock contention - measure the number of processes that are waiting for a lock. More contention -> less parallelism.

We need to chose finer lock granurality to reduce contention, but this increases lock overhead. Release a lock as soon as it is not needed:

1void add_account (int acc_no, int balance) { 2 lock(acc); 3 create_account(acc_no, balance); 4 unlock(acc); 5 6 lock(acc[acc_no]); 7 acc[acc_no] = balance; 8 unlock(acc[acc_no]); 9}

1.6 Read / Write Locks

A race condition occurs when multiple threads or processes read / write shared data and the final result depends on the order of execution.

2 Memory Models

In this course, we assume sequential consistency:

Other memory models (due to hardware behaviour and compiler optimizations) exist.

1int flag0 = 0, flag1 = 0; 2 3void T1() { 4 flag0 = 1; 5 if (flag1 == 0) critical(); 6} 7 8void T2() { 9 flag1 = 1; 10 if (flag0 == 0) critical(); 11}

Under sequential consistency, it is impossible for both threads to enter the critical section. However, under weak memory models, it is possible.

3. Semaphores

Semaphores are blocking synchronization methods, where process cooperate by means of signals. They are special variables, accessible via the following atomic operations:

Semaphores have two private components:

1typedef struct { 2 int value; 3 struct process *queue; 4} semaphore; 5 6void init(semaphore *s, int n) { 7 s->value = n; 8 init_queue(s->queue); 9} 10 11void down(semaphore *s) { 12 s->value--; 13 if (s->value < 0) { 14 add_to_queue(s->queue, current_process); 15 block(); 16 } 17} 18 19void up(semaphore *s) { 20 s->value++; 21 if (s->value <= 0) { 22 pop_from_queue(s->queue, p); 23 unblock(p); 24 } 25}

3.1 Semaphore: Mutual Exclusion

In a binary semaphore, the counter is initialized to 1. This is similar to a lock / mutex.

1semaphore *sema; 2 3void P0() { 4 down(sema); 5 critical_section(); 6 up(sema); 7} 8 9void P1() { 10 down(sema); 11 critical_section(); 12 up(sema); 13} 14 15void init () { 16 init(sema, 1); 17 18 // Start P0 and P1 in random order 19}

3.2 Semaphore: Ordering

Process P0 must run before P1:

1semaphore *sema; 2 3void P0() { 4 critical_section(); 5 up(sema); 6} 7 8void P1() { 9 down(sema); 10 critical_section(); 11} 12 13void init () { 14 init(sema, 0); 15 16 // Start P0 and P1 in random order` 17}

3.3 General Semaphores

The initial value of a semaphore indicates how many processes can access shared data at the same time.

3.4 Producer / Consumer Problem

Producer constraints:

Consumer constraints:

A buffer can hold between and items.

1semaphore *item, *space, *mutex; 2sema_init(item, 0); // Ensure buffer is not empty 3sema_init(space, N); // Ensure buffer is not full 4sema_init(mutex, 1); // Ensure mutual exclusion 5 6void producer() { 7 while (true) { 8 produce(); 9 down(space); 10 down(mutex); 11 deposit(); 12 up(mutex); 13 up(item); 14 } 15} 16 17void consumer() { 18 while (true) { 19 down(item); 20 down(mutex); 21 fetch(); 22 up(mutex); 23 up(space); 24 consume(); 25 } 26}

4. Monitors

A monitor consists of:

Processes can only call entry procedures, and cannot directly access internal data. Only one process can be in the monitor at a time.

Condition variables are associated with higher-level conditions (e.g. "space available in buffer"). It contains three operations:

Signals do not accumulate - if a condition variable is signalled with no one waiting, the signal is lost. Now, the producer / consumer problem can be solved using monitors:

1condition not_empty, not_full; 2int count = 0; 3 4void producer() { 5 while (true) { 6 produce(); 7 if (count == N) wait(not_full); 8 deposit(); 9 count++; 10 if (count == 1) signal(not_empty); 11 } 12} 13 14void consumer() { 15 while (true) { 16 if (count == 0) wait(not_empty); 17 fetch(); 18 count--; 19 signal(not_full); 20 consume(); 21 } 22}
Back to Home