CITS2002 Systems Programming
1 next ¡ú CITS2002 CITS2002 schedule
Introduction to synchronization using mutexes
A core problem with threads sharing the same memory address space, is making sure that they
don’t “step on each other’s toes”:
if multiple threads are simply reading the same variable, there is no problem.
however, if multiple threads update the same variable, the order of those updates – the order in which the threads execute – is important as it affects the values of the variable, over time, and the final value of that variable.
Thread libraries support an abstraction named a mutex, an abbreviation ofmutual exclusion. Mutex variables are one of the primary means providing synchronization of threads – the order in which they execute – and for protecting shared data when multiple writes can occur.
A mutex variable acts like a lock protecting access to a shared data resource.
The basic concept of a mutex as used in pthreads is that only one thread can lock (or own) a
mutex variable at any given time.
Thus, even if several threads try to lock a mutex only one thread will be successful. No other thread can own that mutex until the owning thread unlocks that mutex. Threads must take turns accessing protected data.
CITS2002 Systems Programming, Lecture 19, p1, 5th October 2021.
CITS2002 Systems Programming
¡ûprev 2 next¡ú CITS2002 CITS2002schedule
Race conditions
Mutexes can be used to prevent race conditions. Consider this example of a race condition involving two threads attempting to add funds to a bank account:
Read balance: $1000
Balance $1000 $1000 $1000 $1000 $1200 $1200
Read balance: $1000
Deposit $200
Deposit $200
Update balance $1000+$200
Update balance $1000+$200
In the above example, a mutex variable should be used to lock the balance (variable) while a thread is using this shared resource. Threads owning a mutex typically update global variable(s). When used correctly, the final result (value) is the same as would be observed if all updates were made by a single-threaded program.
The variables being updated belong to a critical section. A typical sequence when using a mutex is as follows:
create and initialize a single mutex variable (shared by all threads), several threads attempt to lock the mutex,
only one thread succeeds and that thread owns the mutex,
the owner thread performs some actions (such as updating a variable), the owner unlocks the mutex,
another thread acquires the mutex and repeats the sequence, and finally, the mutex is destroyed.
CITS2002 Systems Programming, Lecture 19, p2, 5th October 2021.
CITS2002 Systems Programming
¡ûprev 3 next¡ú CITS2002 CITS2002schedule
An example using a mutex
Continuing our banking example, we can see how a mutex may be use to protect a shared resource (here, balance) by requiring threads tolock (own) the mutex before updating the resource:
#include
pthread_mutex_t mutexbalance = PTHREAD_MUTEX_INITIALIZER;
void deposit(int amount) {
pthread_mutex_lock(&mutexbalance); {
int currbalance = balance;
currbalance += amount;
balance = currbalance; // write shared balance
pthread_mutex_unlock(&mutexbalance); }
void withdraw(int amount) {
pthread_mutex_lock(&mutexbalance); {
// read shared balance balance = currbalance; // write shared balance
int currbalance = balance; if(currbalance >= amount) {
currbalance -= amount;
pthread_mutex_unlock(&mutexbalance); }
create threads…
deposit(200);
withdraw(100);
deposit(500);
withdraw(200);
// thread 1
// thread 2
// thread 1
// thread 3
// read shared balance
CITS2002 Systems Programming, Lecture 19, p3, 5th October 2021.
CITS2002 Systems Programming
¡ûprev 4 next¡ú CITS2002 CITS2002schedule
Pthreads functions to manage mutexes Creating and destroying:
Mutex variables must be declared with type pthread_mutex_t, and be initialized before being used. A mutex is initially unlocked. There are two ways to initialize a mutex variable:
1. statically, when declared. e.g. pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
2. dynamically, with pthread_mutex_init(), which permits setting detailed mutex object attributes (the protocol used to prevent priority inversions, the priority ceiling of a mutex, and how mutexes may be shared).
pthread_mutex_destroy() may be used when a mutex object which is no longer needed (or, like dynamic memory, will be deallocated when the whole process terminates).
Locking and unlocking:
The pthread_mutex_lock() function acquires a lock on the specified mutex variable. If the mutex is already locked by another thread, this call will block the calling thread until the mutex is unlocked.
pthread_mutex_trylock() will attempt to lock a mutex. However, if the mutex is already locked, the routine will return immediately with a busy error code. This function may be useful in preventing deadlock conditions.
pthread_mutex_unlock() will unlock a mutex if called by the owning thread, after it has completed its use of protected data. An error will be returned if:
If the mutex was already unlocked
If the mutex is owned by another thread
There is nothing “magical” about mutexes, they are just an agreement between participating threads. It is up to the programmer to insure that all necessary threads make the mutex lock and unlock calls correctly.
CITS2002 Systems Programming, Lecture 19, p4, 5th October 2021.
CITS2002 Systems Programming
¡ûprev 5 next¡ú CITS2002 CITS2002schedule
Condition variables
Condition variables provide another way for threads to synchronize. While mutexes implement synchronization by controlling thread access to data, condition variables allow threads to synchronize based upon the actual value of data.
Without condition variables, we would require threads continually poll (possibly in a critical section), to check if a condition is met – very resource intensive! A condition variable helps avoid the polling.
A condition variable is always used in conjunction with a mutex lock.
Consider this sequence in which threadB informs threadA when a required condition is met:
main thread:
main thread:
declare and initialize global variables requiring synchronization declare and initialize a condition variable object
declare and initialize an associated mutex
create threads A and B to execute in parallel
execute to the point where a certain condition must occur
(a variable reaches a required value)
lock associated mutex and check value of a global variable
call pthread_cond_wait() to perform a blocking wait for signal from threadB The call to pthread_cond_wait() automatically and atomically unlocks the associated mutex variable so that it can be used by threadB
when signalled, wake up; mutex is automatically and atomically locked
explicitly unlock mutex continue execution
lock associated mutex
update the value of the global variable that threadA is waiting on
check value of the global threadA wait variable – if it fulfills the desired condition, signal threadA
unlock mutex continue execution.
join threadA or threadB, or continue execution…
CITS2002 Systems Programming, Lecture 19, p5, 5th October 2021.
CITS2002 Systems Programming
¡ûprev 6 next¡ú CITS2002 CITS2002schedule
Pthreads functions to manage condition variables Creating and destroying:
Condition variables are declared of type pthread_cond_t, and must be initialized before being used. There are two ways to initialize a condition variable:
1. Statically, when it is declared. e.g. pthread_cond_t myconvar = PTHREAD_COND_INITIALIZER;
2. Dynamically, with the pthread_cond_init() function.
thread_cond_destroy() should be used to free a condition variable that is no longer needed.
Waiting and signalling:
pthread_cond_wait() blocks the calling thread until the specified condition is signalled. This function is called while the mutex is locked,’ and will automatically release the mutex while it waits. After the signal is received and thread is awakened, the mutex is automatically locked for use by the thread. That thread is responsible for unlocking mutex finished with it.
Using a while loop instead of an if statement to check the waited for condition can help deal with several potential problems, such as:
if several threads are waiting for the same wake up signal, they will take turns acquiring the mutex, and any of them can then modify the condition they all waited for,
if the thread received the signal in error due to a program bug, and
the pthreads library may issue spurious wake ups to a waiting thread (without violating the standard!)
The pthread_cond_signal() function is used to signal (or wake up) another thread waiting on the condition variable. It must be called after the mutex is locked, and must unlock the mutex in order for pthread_cond_wait() function to complete. pthread_cond_broadcast() should be used instead of if more than one thread is in a blocking wait state.
It is a logical error to call pthread_cond_signal() before calling pthread_cond_wait(). Proper locking and unlocking of the associated mutex variable is essential when using
these functions. For example:
failing to lock the mutex before calling pthread_cond_wait() may cause it NOT to block.
failing to unlock the mutex after calling pthread_cond_signal() may not allow a matching pthread_cond_wait() call to complete (it will remain blocked).
CITS2002 Systems Programming, Lecture 19, p6, 5th October 2021.
CITS2002 Systems Programming
¡ûprev 7 next¡ú CITS2002 CITS2002schedule
A condition variable example
Let’s consider a typical ‘producer/consumer’ example, in which the producer() thread create items and makes them available, and theconsumer() thread uses them.
This is a very typical problem requiring synchronization as we don’t wish either thread to block indefinitely waiting for the other, and the consumer should only execute when it’s known that there’s at least one new item available:
#include
pthread_cond_t cond_recv pthread_cond_t cond_send
= PTHREAD_COND_INITIALIZER; = PTHREAD_COND_INITIALIZER;
pthread_mutex_t cond_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
bool full=false; int count=0;
void *producer(void *thread_id) {
while(true) { pthread_mutex_lock(&cond_mutex);
while(full) {
pthread_cond_wait(&cond_recv, &cond_mutex) ;
pthread_mutex_unlock(&cond_mutex); pthread_mutex_lock(&count_mutex);
full = true;
printf(“producer(%li):%i\n”, pthread_self() , count);
pthread_cond_broadcast(&cond_send); pthread_mutex_unlock(&count_mutex);
if((count >= MAX_ITEMS) { break;
void *consumer(void *thread_id) {
while(true) { pthread_mutex_lock(&cond_mutex);
while(!full) {
pthread_cond_wait(&cond_send , &cond_mutex) ;
pthread_mutex_unlock(&cond_mutex); pthread_mutex_lock(&count_mutex);
full = false;
printf(“consumer(%li):%i\n”, pthread_self(), count);
pthread_cond_broadcast(&cond_recv); pthread_mutex_unlock(&count_mutex);
if((count >= MAX_ITEMS) { break;
CITS2002 Systems Programming, Lecture 19, p7, 5th October 2021.
CITS2002 Systems Programming
¡û prev 8 CITS2002 CITS2002 schedule
Thread support in C11
All of the examples in this and the previous lecture have used examples involving functions from the pthreads API.
Threading support has been long overdue in the standard C language specification, and it was finally defined in C11.
Before that, the POSIX threads API was used as the primary tool to utilize multi-threaded programming. Since C11 provided a more standard interface that could be used without platform dependencies, it has been recommended to use the ISO language API than the POSIX version. Although the two APIs don’t match in their function prototypes, the main features are very similar.
Note that all additions have been the standard C11 library, and have not added new keywords to the language.
Some helpful references:
cppreference.com
Youtube video by (10 minutes).
Discussion comparing POSIX threads and C11 threads[Reddit.com]
And a clever use of (only) a C header file to declare all C11 thread functions ‘on top of’ an existing pthreads implementation: c11threads.h
CITS2002 Systems Programming, Lecture 19, p8, 5th October 2021.