![]() |
CITS2002 Systems Programming |
![]() |
![]() |
|||||
Introduction to synchronization using mutexesA core problem with threads sharing the same memory address space, is making sure that they don't "step on each other's toes":
Thread libraries support an abstraction named a mutex, an abbreviation of mutual 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 21, p1, 10th October 2023.
Race conditionsMutexes 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:
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:
CITS2002 Systems Programming, Lecture 21, p2, 10th October 2023.
An example using a mutexContinuing our banking example, we can see how a mutex may be use to protect a shared resource (here, balance) by requiring threads to lock (own) the mutex before updating the resource:
CITS2002 Systems Programming, Lecture 21, p3, 10th October 2023.
Pthreads functions to manage mutexesCreating 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:
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:
There is nothing "magical" about mutexes, they are just an agreement between participating threads. It is up to the programmer to ensure that all necessary threads make the mutex lock and unlock calls correctly.
CITS2002 Systems Programming, Lecture 21, p4, 10th October 2023.
Condition variablesCondition 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 to 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.
main thread:
CITS2002 Systems Programming, Lecture 21, p5, 10th October 2023.
Pthreads functions to manage condition variablesCreating 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:
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 when 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:
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:
CITS2002 Systems Programming, Lecture 21, p6, 10th October 2023.
A condition variable exampleLet's consider a typical 'producer/consumer' example, in which the producer() thread create items and makes them available, and the consumer() 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:
CITS2002 Systems Programming, Lecture 21, p7, 10th October 2023.
Thread support in C11All 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 to the standard C11 library, and have not added new keywords to the language. Some helpful references:
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 21, p8, 10th October 2023.
|