mario::konrad
programming / C++ / sailing / nerd stuff
Multithreading: Tutorial 02
© 2002 / Mario Konrad

Why is Synchronisation needed?

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.

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.

Deadlock

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

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).

What is a Mutex?

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:

The critical section usually looks like this:

  1. lock the mutex
  2. do the critical stuff
  3. unlock the mutex

In most cases it is very useful to encapsulate the critical section within a separate function. This enhances readability and maintainability.

The Program

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):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#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;
}

Walkthrough

First, we have to define a mutex variable for further use.

4
    pthread_mutex_t mtx;

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:

6
7
8
9
10
11
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:

18
    print((int)ptr, i);

All there is left to do is within the function main to take care of the initialisation

28
    pthread_mutex_init(&mtx, 0);

and the destruction of the mutex variable:

35
    pthread_mutex_destroy(&mtx);

Download Source Code

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.