Title: COMP309/509 - Lecture 10
class: middle, center, inverse
COMP309/509 - Parallel and Distributed Computing
Lecture 10 - Multithreaded Programming with Mutual Exclusion
By Mitchell Welch
University of New England
Reading
- Chapters 11 and 15 (Section 15.8) from Advanced Programming in the UNIX environment
- Assignment 1 Description
Summary
- Thread Synchronization
- Mutexs - Mutual Exclusion Devices
- Mutex Motivation Examples
- Synchronization: Producers and Consumers
Thread Synchronisation
- As mentioned in lectures 7/8, threads share basically all of the resources of the process they are running on.
- As a result things start to get tricky when multiple threads read and write to the same location.
- A motivating example:
- Thread A reads a shared variable, then proceeds to write to the variable.
- The write operation takes multiple memory cycles and if thread B reads the variable between the memory cycles, then it will see an inconsistent value
Thread Synchronisation
.center[
]
Thread Synchronisation
- To solve this problem, threads must a Mutual Exclusion Device that will only allow a single thread to access the variable at any given time:
.center[
]
Thread Synchronisation
- Another scenario to consider is the when two(or more) threads try to update almost the same time without synchronising.
- This can lead to very inconsistent behaviour due to lost updates
Thread Synchronisation
.center[
]
Thread Synchronisation
Mutexes - Mutual Exclusion Devices
Mutexes - Mutual Exclusion Devices
- A thread holds a mutex if it has successfully locked that mutex.
- Only one thread at any one time can hold a mutex.
- Mutexes are used to protect critical sections.
- Critical Sections are where threads are updating shared data.
- The updating thread should be forced to hold a mutex before updating.
- releasing (unlocking) the mutex when done.
Mutexes - Mutual Exclusion Devices
- Using mutexes requires following a fixed protocol.
- For each piece of distinct shared data:
- Allocate and initialize a mutex.
- Before manipulating that piece of shared data the thread should obtain the lock on the mutex.
- Once the thread has finished manipulating the data, it should release the lock.
Mutexes - Mutual Exclusion Devices
- The five important routines are:
pthread_mutex_init
pthread_mutex_lock
pthread_mutex_unlock
pthread_mutex_trylock
pthread_mutex_destroy
Mutexes - Mutual Exclusion Devices
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr_t attr);
mutex
contains the mutex created.
attr
is almost always:
-
pthread_mutexattr_default
pthread_mutex_init
returns 0 if successful, or an error number to indicate an error has occurred.
Mutexes - Mutual Exclusion Devices
pthread_mutex_init(&lock, NULL);
- Can also, only at the time of declaration, write:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
- Though this form is only for top-level declarations (i.e. static allocation of mutexes).
Mutexes - Mutual Exclusion Devices
- We’ll look more closely at attributes later.
- Linux provides three kinds of mutex:
- fast,
- recursive, or
- error checking.
- The kind of a mutex determines whether it can be locked again by a thread that already owns it.
- The default kind is fast.
- To get a recursive version one can do:
pthread_mutex_t lock = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
- Try man pthread_mutex_init for more details
Mutexes - Mutual Exclusion Devices
- You should only use the shorthand forms:
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
- At the top level of a file, not in the activation record of some function call.
- At the time of declaration.
Mutexes - Mutual Exclusion Devices
- Consequently one needs to learn the other way, since mutexes are often incorporated into structs or arrays.
- An example:
pthread_mutex_t mutex;
...
pthread_mutexattr_t type;
pthread_mutexattr_init(&type);
pthread_mutexattr_settype(&type, PTHREAD_MUTEX_RECURSIVE_NP);
pthread_mutex_init(&mutex, &type);
Mutexes - Mutual Exclusion Devices
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock
locks an unlocked mutex.
- If the mutex is locked by another thread, this routine causes the thread to wait for the mutex to become available.
- If the mutex is already locked by the calling thread, the behavior of
pthread_mutex_lock
depends on the kind of the mutex.
- I.e. it’s attributes, whether it is: fast, recursive or error checking.
Mutexes - Mutual Exclusion Devices
If pthread_mutex_lock
is used to lock a mutex that is already locked by the calling thread, then:
- If the mutex is of the fast kind, the calling thread is suspended until the mutex is unlocked, thus effectively causing the calling thread to deadlock.
- If the mutex is of the error checking kind, the call returns immediately with the error code EDEADLK.
- If the mutex is of the recursive kind, the call succeeds and returns immediately, recording the number of times the calling thread has locked the mutex.
An equal number of pthread_mutex_unlock
operations must be performed before the mutex returns to the unlocked state.
Mutexes - Mutual Exclusion Devices
#include <pthread.h>
int pthread_mutex_unlock(phread_mutex_t *mutex);
- The mutex should have been locked by the thread unlocking it.
- If this is not the case, behaviour depends on what sort of mutex it is. Linux is non-standard here!
- Read
man pthread_mutex_unlock
for gory details, here.
- In the case of error checking this will result in an error.
- If other threads are waiting to lock this mutex, then one of them will subsequently succeed.
- Which one is entirely non-deterministic.
Mutexes - Mutual Exclusion Devices
#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_trylock()
locks a mutex.
- If the mutex is already locked, the calling thread does not wait for the mutex to become available.
- Instead, pthread_mutex_trylock returns immediately with the error code EBUSY.
- Is useful if the thread has something better to do than wait for the mutex.
Mutex Motivation Examples
- Consider the following program:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define THREADS 5
static int sum = 1;
void *updater(void *ptr){
sum = sum + 1;
pthread_exit(NULL);
}
int main(void){
int i;
pthread_t threads[THREADS];
for(i = 0; i < THREADS ; i++)
pthread_create(&threads[i],NULL,updater,NULL);
for(i = 0; i < THREADS ; i++)
pthread_join(threads[i],NULL);
fprintf(stderr, "sum = %d\n", sum);
exit(EXIT_SUCCESS);
}
Mutex Motivation Examples
- Presumably it is designed so that after execution sum should be incremented THREADS times.
- Thus it should print out:
sum = 1 + THREADS
- In reality this does seem to work almost all the time.
- However it is at the mercy of the scheduler.
- In a unfortunate world the answer could be any number greater than 1 and no bigger than 1 + THREADS.
- Pretty dangerous.
Mutex Motivation Examples
- Review bad_example02.c for a slightly more sophisicated, but equally cringe-worthy attempt.
- This does seem to work almost all the time, however this is very in-efficient.
void spin(){
int j;
for(j=0; j< delay; j++);
}
void *updater(void *ptr){
int i;
spin(); i = sum;
spin(); i++;
spin(); sum = i;
spin(); pthread_exit(NULL);
}
Mutex Motivation Examples
Examples> example02a 100 100
sum = 101
Examples> example02a 100 10000
sum = 55
Examples> example02a 100 1000000
sum = 34
Examples> example02a 100 10000000
sum = 20
Examples> example02a 100 100000000
sum = 3
Examples>
Mutex Motivation Examples
pthread_mutex_t sum_lock;
void *updater(void *ptr){
pthread_mutex_lock(&sum_lock);
sum = sum + 1;
pthread_mutex_unlock(&sum_lock);
pthread_exit(NULL);
}
Mutex Motivation Examples
- You can also use a macro to statically allocate the mutex lock - good_example02.c
static pthread_mutex_t sum_lock = PTHREAD_MUTEX_INITIALIZER;
Mutex Motivation Examples
void spin(){
int j;
for(j=0; j< delay; j++);
}
void *updater(void *ptr){
int i;
spin(); pthread_mutex_lock(&sum_lock);
spin(); i = sum;
spin(); i++;
spin(); sum = i;
spin(); pthread_mutex_unlock(&sum_lock);
spin(); pthread_exit(NULL);
}
Synchronization: Producers and Consumers
- Mutual exclusion does not solve the lost update issue.
- A generic synchronization problem not solved by mutexes is the so called data race.
- The simplest example of a data race is provided by the producer and consumer problem.
Synchronization: Producers and Consumers
- We will explore the producer and consumer problem use a circular buffer.
- We start with operations for manipulating a circular buffer with BUFSIZE slots.
- As usual I’ll try and lead by example as far as separate compilation goes.
Synchronization: Producers and Consumers
#include <pthread.h>
#include <unistd.h>
#define BUFSIZE 8
static int buffer[BUFSIZE];
static int bufin = 0;
static int bufout = 0;
static pthread_mutex_t
buffer_lock = PTHREAD_MUTEX_INITIALIZER;
int get_buffersize(){
return BUFSIZE;
}
void get_item(int *itemp){
pthread_mutex_lock(&buffer_lock);
*itemp = buffer[bufout];
bufout = (bufout + 1) % BUFSIZE;
pthread_mutex_unlock(&buffer_lock);
return;
}
void put_item(int item){
pthread_mutex_lock(&buffer_lock);
buffer[bufin] = item;
bufin = (bufin + 1) % BUFSIZE;
pthread_mutex_unlock(&buffer_lock);
return;
}
Synchronization: Producers and Consumers
.center[
]
Synchronization: Producers and Consumers
- In the simplest form of producer consumer problem:
- We have (at least two threads).
- A producer that put things into the slots.
- A consumer that takes things out of the slots.
- The threads use the access functions defined above.
- Since access to the buffer is protected by a mutex, the producer and consumer are prevented from stepping on each others toes by either:
- removing something while it is still be put in.
- putting something in while the previous entry is being extracted.
Synchronization: Producers and Consumers
- Consider the simple minded implementation of the producer/consumer in simple.c
void *producer(void * arg1){
int i;
for (i = 1; i <= SUMSIZE; i++)
put_item(i*i);
pthread_exit(NULL);
}
void *consumer(void *arg2){
int i, myitem;
for (i = 1; i <= SUMSIZE; i++) {
get_item(&myitem);
sum += myitem; }
pthread_exit(NULL);
}
Synchronization: Producers and Consumers
- The problem with this example is that there is absolutely no synchronization between the consumer and the producer.
- As a consequence things could go horribly wrong.
For example:
- the consumer could get ahead of the producer and remove items before they have been put in place.
- the producer could get carried away and place items in slots, overwriting entries that have yet to be consumed.
- In fact on turing if the consumer is created first the sum is 0, while if the producer is created first the answer errs on the other side.
- To handle this type of synchronization we use POSIX condition variables.
Summary
- Thread Synchronization
- Mutexs - Mutual Exclusion Devices
- Mutex Motivation Examples
- Synchronization: Producers and Consumers
Reading
- Chapters 11 and 15 (Section 15.8) from Advanced Programming in the UNIX environment
- Assignment 1 Description