Whenever two or more active parts have to share resources, sooner or later appear conflicts about who uses when a shared resource. Synchronisation of those active parts makes it possible to control the access to the resources and lets run the application stable and reliable.
Stability and reliability are very important attributes when it comes to let several therads running concurrently. Assuming the mutual exclusion is guaranteed, there are two problems we have to take care of:
Read below to learn more about those problems.
As mentioned above there are several problems the programmer has to take care of. This section covers those problems and how they can occur.
What is a deadlock? A deadlock is the situation in which all threads are blocked and waiting for a resource. The picture below shows how this situation can happen:
The thread X holds the resouce A, the thread Y holds the resource B. Now the thread X wants to take the resource B and get blocked, because thread Y is already using it. How comes, thread Y likes to get the resource A as well and get blocked too, because thread X already holds it. This is a deadlock situation, because neither thread X nor thread Y is able to continue its work.
It is necessary to avoid this situation at any cost. To gain performance at the cost of the risk of a deadlock is a bad choice.
One possibility to avoid deadlock is to synchronize the threads and let them fetch the resources using timeouts instead of blocking. This means that the thread which likes to get the resource will fail after an amount of time. This means of course a good exception/error handling.
Indefinite suspension can occur if one thread is of lower priority than another and the scheduler run always the thread with the highest priority (not all schedulers do so). If the high priority thread has a big workload, the lower priority thread does not get CPU time any more, for an indefinite amount of time.
This is mostly an issue of design. A good design would be, that your program does run (logically) even if you don’t consider the priorities. They should only be for making adjustments in response times (in theory).
A the name says: mutual exclusion is a mechanism to capsule a part of the program, so called critical sections. It makes sure, that only one thread can be inside of a critical section. Of course if there are several critical section, each guarded by a independent mutex, it is possible to have a thread X in the section A and another thread Y in the section B.
If a thread is within the critical section guarded by a mutex, it is called “the thread is holding the mutex”.
The consequence of that mechanism is, that the programmer should try to keep the cirital sections as short and simple as possible. Not to respect this rule means a possible (probably) loss of (a lot of) performance.
Mainly there are three operations on a mutex, which are used for daily business:
lock
: locks the mutexunlock
: unlocks the mutextrylock
: tries to lock the mutex, non-blocking function. If the mutex is already locked, the function will return immediatly providing an error code. In the other case, the mutex is locked and the thread may proceed.The critical section usually looks like this:
In most cases it is very useful to encapsulate the critical section within a separate function. This enhances readability and maintainability.
Enough for the theory, let’s have a look at an example.
We start with the program we made in the last tutorial (thread1.c
):
#include <stdio.h>
#include <pthread.h>
void * work(void * ptr)
{
int i;
for (i = 0; i < 10; i++)
{
printf("%d", (int)ptr);
usleep(1000);
}
pthread_exit(0);
}
int main(int argc, char ** argv)
{
pthread_t t0, t1;
pthread_create(&t0, 0, work, (void *)0);
pthread_create(&t1, 0, work, (void *)1);
pthread_join(t0, 0);
pthread_join(t1, 0);
return 0;
}
In the function work there is a printf statement which prints out the specified number for the thread. Now we like to write more than one character. but since the function printf is not thread safe, we have to take care of problem by ourselfes.
To reach our goal we’re using a mutex:
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mtx;
void print(int thread, int i)
{
pthread_mutex_lock(&mtx);
printf("thread %d: %d\n", thread, i);
pthread_mutex_unlock(&mtx);
}
void * work(void * ptr)
{
int i;
for (i = 0; i < 10; i++)
{
print((int)ptr, i);
usleep(1000);
}
pthread_exit(0);
}
int main(int argc, char ** argv)
{
pthread_t t0, t1;
pthread_mutex_init(&mtx, 0);
pthread_create(&t0, 0, work, (void *)0);
pthread_create(&t1, 0, work, (void *)1);
pthread_join(t0, 0);
pthread_join(t1, 0);
pthread_mutex_destroy(&mtx);
return 0;
}
First, we have to define a mutex variable for further use.
Since we like to encapsulate the critical section within a separate function to increase readability, let’s do so and we have to protect the critical section with locking and unlocking the mutex:
void print(int thread, int i)
{
pthread_mutex_lock(&mtx);
printf("thread %d: %d\n", thread, i);
pthread_mutex_unlock(&mtx);
}
The thread function a small change to according to the separated critical section:
All there is left to do is within the function main to take care of the initialisation
and the destruction of the mutex variable:
All source code files provided by this page is free to copy, modify, redistribute and use for any purpose. The tutorial is copyrighted by Mario Konrad.