Knowing how to properly use threads should be part of every computer science and engineering student repertoire. This tutorial is an attempt to help you become familiar with multi-threaded programming with the POSIX (Portable Operating System Interface) threads, or pthreads. This tutorial explains the different tools defined by the pthread library, shows how to use them, and gives examples of using them to solve real life programming problems.
Technically, a thread is defined as an independent stream of instructions that can be scheduled to run as such by the operating system.
A thread is a semi-process that has its own stack, and executes a given piece of code. Unlike a real process, the thread normally shares its memory with other threads (where as for processes we usually have a different memory area for each one of them). A Thread Group is a set of threads all executing inside the same process. They all share the same memory, and thus can access the same global variables, same heap memory, same set of file descriptors, etc. All these threads execute in parallel (i.e. using time slices, or if the system has several processors, then really in parallel).
Single- and Multi-Threaded Processes
In order to take full advantage of the capabilities provided by threads, a standardized programming interface was required. For UNIX systems, this interface has been specified by the IEEE POSIX 1003.1c standard (1995). Implementations which adhere to this standard are referred to as POSIX threads, or Pthreads. Most hardware vendors now offer Pthreads in addition to their proprietary threads.
If implemented correctly, threads have some advantages over processes. Compared to the standard fork(), threads carry a lot less overhead.
Remember that fork() produces a second copy of the calling process. The parent and the child are completely independent, each with its own address space, with its own copies of its variables, which are completely independent of the same variables in the other process.
Threads share a common address space, thereby avoiding a lot of the inefficiencies of multiple processes.
On the other hand, because threads in a group all use the same memory space, if one of them corrupts the contents of its memory, other threads might suffer as well. With processes, the operating system normally protects processes from one another, and thus if one corrupts its own memory space, other processes won't suffer.
Example 1: A responsive user interface
One area in which threads can be very helpful is in user-interface programs. These programs are usually centered around a loop of reading user input, processing it, and showing the results of the processing. The processing part may sometimes take a while to complete, and the user is made to wait during this operation. By placing such long operations in a separate thread, while having another thread to read user input, the program can be more responsive. It may allow the user to cancel the operation in the middle.
Example 2: A graphical interface
In graphical programs the problem is more severe, since the application should always be ready for a message from the windowing system telling it to repaint part of its window. If it's too busy executing some other task, its window will remain blank, which is rather ugly. In such a case, it is a good idea to have one thread handle the message loop of the windowing systm and always ready to get such repain requests (as well as user input). Whenever this thread sees a need to do an operation that might take a long time to complete (say, more then 0.2 seconds in the worse case), it will delegate the job to a separate thread.
Example 3 : A Web server
When a multi-threaded program starts executing, it has one thread running,
which executes the main() function of the program. This is already a
full-fledged thread, with its own thread ID. In order to create a new thread,
the program should use the
pthread_create() function.
Here is how to use it:
#include <stdio.h> /* standard I/O routines */
#include <pthread.h> /* pthread functions and data structures */
/* function to be executed by the new thread */
void* PrintHello(void* data)
{
int my_data = (int)data; /* data received by thread */
printf("Hello from new thread - got %d\n", my_data);
pthread_exit(NULL); /* terminate the thread */
}
/* like any C program, program's execution begins in main */
int main(int argc, char* argv[])
{
int rc; /* return value */
pthread_t thread_id; /* thread's ID (just an integer) */
int t = 11; /* data passed to the new thread */
/* create a new thread that will execute 'PrintHello' */
rc = pthread_create(&thread_id, NULL, PrintHello, (void*)t);
if(rc) /* could not create thread */
{
printf("\n ERROR: return code from pthread_create is %d \n", rc);
exit(1);
}
printf("\n Created new thread (%d) ... \n", thread_id);
pthread_exit(NULL); /* terminate the thread */
}
Let us examine the simple threaded program above. While it does not do anything useful, it will help you understand how threads work. Let us take a step by step look at what the program does.
main() we declare a variable called
thread_id, which
is of type pthread_t.
This is basically an integer used to identify the
thread in the system.
After declaring thread_id, we call the pthread_create()
function to create a real, living thread.
pthread_create()
gets 4 arguments The first argument is
a pointer to thread_id, used by pthread_create()
to supply the program with the thread's identifier.
The second argument is used to set some
attributes for the new thread. In our case we supplied a NULL pointer to
tell pthread_create() to use the default values.
Notice that PrintHello() accepts a void * as an argument and
also returns a void * as a return value. This shows us that it is possible
to use a void * to pass an arbitrary piece of data to our new thread, and
that our new thread can return an arbitrary piece of data when it finishes.
How do we pass our thread an arbitrary argument? Easy. We use the fourth
argument to the pthread_create() call. If we do not want
to pass any data to the new thread, we set the fourth argument to NULL.
pthread_create() returns zero on success and a non-zero value
on failure.
pthread_create() successfully returns, the program will consist of
two threads. This is because the main program
is also a thread and it executes the code in the
main() function in parallel to the thread it creates.
Think of it this way: if you write a program that does not use POSIX threads
at all, the program will be single-threaded (this single thread is called
the "main" thread).
pthread_exit() causes the current thread
to exit and free any thread-specific resources it is taking.
In order to compile a multi-threaded program using gcc,
we need to link it with the pthreads library. Assuming you have this library
already installed on your system, here is how to compile our first program:
The source code for this program may be found in the
hello.c file.
gcc hello.c -o hello -lpthread
pthreads in your Unix account and download hello.c
into the pthreads directory. Compile the source code and run the
hello executable. The ouput should be similar to
Created new thread (4) ...
Hello from new thread - got 11
pthread_self(), which returns the thread id:
pthread_t pthread_self();
Use it as
pthread_t tid;
tid = pthread_self();
Modify the code for hello.c to print out the thread id for both
threads. Recompile and run the hello executable.
The new ouput should be similar to
I am thread 1. Created new thread (4) ...
Hello from new thread 4 - got 11
Now modify the code so that the main thread passes its own thread id to the
new thread it creates. Recompile and run the hello executable.
The ouput should be similar to
I am thread 1. Created new thread (4) ...
Hello from new thread 4 - got 1
pthread_exit routine. In this exercise, modify the your
hello.c program as follows.
In the PrintHello routine, add a line before the printf
call which looks like sleep(1);. This should be the first line of the
function.
In the main function, comment out the last statement line which contains
the pthread_exit call. Recompile and run the hello
executable.
What happens? Why?
Now, put the pthread_exit call back in the main program, but remove
it from the PrintHello routine. Also add the sleep call
to the main routine, just before the second printf call, and remove
it from the PrintHello routine. Recompile and run the hello
executable.
What happens? Why?
It is necessary to use pthread_exit at the end of the main program.
Otherwise, when it exits, all running threads will be killed.
pthread_create()
splits our single thread into two threads,
pthread_join() merges two threads into a
single thread. The pthread_join
subroutine blocks the calling thread until the thread with identifier equal to the first
argument terminates.
#include <stdio.h> /* standard I/O routines */
#include <pthread.h> /* pthread functions and data structures */
void* PrintHello(void* data)
{
pthread_t tid = (pthread_t)data; /* data received by thread */
pthread_join(tid, NULL); /* wait for thread tid */
printf("Hello from new thread %d - got %d\n", pthread_self(), data);
pthread_exit(NULL); /* terminate the thread */
}
/* like any C program, program's execution begins in main */
int main(int argc, char* argv[])
{
int rc; /* return value */
pthread_t thread_id; /* thread's ID (just an integer) */
int tid;
tid = pthread_self();
rc = pthread_create(&thread_id, NULL, PrintHello, (void*)tid);
if(rc) /* could not create thread */
{
printf("\n ERROR: return code from pthread_create is %d \n", rc);
exit(1);
}
sleep(1);
printf("\n Created new thread (%d) ... \n", thread_id);
pthread_exit(NULL);
}
The first argument to pthread_join() is the identifier of the thread to join.
The second argument is a pointer to a void pointer.
pthread_join(pthread_t tid, void * return_value);
If the return_value pointer is non-NULL, pthread_join will place
at the memory location pointed to by return_value,
the value passed by the thread tid through the pthread_exit call.
Since we don't care about return value of the main thread, we set it to NULL.
Recompile and run the executable for the above code. Is the otuput what you expected?
hellomany.c
that will create a number N of threads specified in the command line, each of which
prints out a hello message and its own thread ID. To see how the execution of the
threads interleaves, make the main thread sleep for 1 second for every 4 or 5 threads
it creates. The output of your code should be similar to:
I am thread 1. Created new thread (4) in iteration 0...
Hello from thread 4 - I was created in iteration 0
I am thread 1. Created new thread (6) in iteration 1...
I am thread 1. Created new thread (7) in iteration 2...
I am thread 1. Created new thread (8) in iteration 3...
I am thread 1. Created new thread (9) in iteration 4...
I am thread 1. Created new thread (10) in iteration 5...
Hello from thread 6 - I was created in iteration 1
Hello from thread 7 - I was created in iteration 2
Hello from thread 8 - I was created in iteration 3
Hello from thread 9 - I was created in iteration 4
Hello from thread 10 - I was created in iteration 5
I am thread 1. Created new thread (11) in iteration 6...
I am thread 1. Created new thread (12) in iteration 7...
Hello from thread 11 - I was created in iteration 6
Hello from thread 12 - I was created in iteration 7
Exercise 6. If a thread wants to pass multiple arguments to
another thread it creates, it needs to use a structure. Structures are declared in C as follows:
The piece of code above defines a new datatype, called struct Example
{
int my_int;
char my_char;
int * my_int_pointer;
};
struct Example.
You can now create variables of this type using
and access fields of the structure using the dot notation:
struct Example name_of_variable;
In case of pointer variables of this type
name_of_variable.my_int = 5;
name_of_variable.my_char = 'a';
name_of_variable.my_int_pointer = &(name_of_variable.my_int);
you can access fields of the structure using the arrow operator:
struct Example * pointer_variable;
Compile, run and understand the
pointer_variable->my_int = 5;
pointer_variable->my_char = 'a';
pointer_variable->my_int_pointer = &(pointer_variable->my_int);
hellostruct.c
pthreads program that illustrates a safe way to
pass multiple arguments to threads during thread creation, using structures.
Synchronizing threads with mutexes
Now it is time to take a look at some code that does something a little
unexpected. The program threadadd.c creates
a new thread. Both the main thread and the new thread increment a global
variable called myglobal 20 times.
But the program produces unexpected results.
threadadd.c
and observe the ouput of the program.
Quite unexpected! Since myglobal starts at 0, and both the main thread
and the new thread increment it by 20, we should see myglobal equal
40 at the end of the program. Something fishy is going on here.
Threads can greatly simplify writing elegant and efficient programs. However,
there are problems when multiple threads share a common address
space, like the variable myglobal in our earlier example.
To understand what might happen, consider the following code:
THREAD 1 THREAD 2
a = data; b = data;
a++; b--;
data = a; data = b;
Now if this code is executed serially (for instance, THREAD 1 first and then THREAD 2), there are no problems. However threads execute in an arbitrary order, so consider the following situation:
THREAD 1 THREAD 2
a = data;
b = data;
a++;
b--;
data = a;
data = b;
[data = data - 1!!!!!!!]
So data could end up +1, 0, -1, and there is NO WAY to know which value! It is completely non-deterministic!
The solution to this is to provide functions that will block a thread if another thread is accessing data that it is using.
Pthreads use a data type called a mutex to achieve this.
A basic mechanism supplied by the pthreads library to solve the data race problem, is called a mutex. Mutexes have two basic operations, lock and unlock. If a mutex is unlocked and a thread calls lock, the mutex locks and the thread continues. If however the mutex is locked, the thread blocks until the thread holding the lock calls unlock.
Locking a mutex is an atomic operation, meaning that the operating system (or threads library) assures you that if you locked a mutex, no other thread succeeded in locking this mutex at the same time.
In order to create a mutex, we first need to declare a variable of type
pthread_mutex_t and then initialize it using
the function
int pthread_mutex_init (pthread_mutex_t *mut, const pthread_mutexattr_t *attr);
The first argument is a pointer to the mutex. To second argument is
used to set the mutex attributes. To use the default
mutex attributes, just pass NULL to it.
pthread_mutex_t a_mutex;
pthread_mutex_init (&a_mutex, NULL);
In order to lock a mutex, we may use the function
pthread_mutex_lock().
This function attempts to lock the mutex,
or block the thread if the mutex is already locked by another thread. In this
case, when the mutex is unlocked by the first thread, the function will return
with the mutex locked by our thread. Here is how to lock a mutex (assuming it
was initialized earlier):
if (rc) { /* an error has occurred */
perror("pthread_mutex_lock");
pthread_exit(NULL);
}
/* mutex is now locked - do your stuff. */
int rc = pthread_mutex_lock(&a_mutex);
After the thread did what it had to (change variables or data structures,
handle file, or whatever it intended to do), it should free the mutex,
using the pthread_mutex_unlock() function,
like this:
if (rc) {
perror("pthread_mutex_unlock");
pthread_exit(NULL);
}
rc = pthread_mutex_unlock(&a_mutex);
After we finished using a mutex (that is, no thread needs it at all), we should
destroy it. However, if only one thread finished with the mutex,
it should leave it alive for the other threads that might still need to use
it. Once all finished using it, the last one can destroy it using the
pthread_mutex_destroy() function:
rc = pthread_mutex_destroy(&a_mutex);
After this call, this variable (a_mutex) may not be used as a mutex any more,
unless it is initialized again. Thus, if one destroys a mutex too early,
and another thread tries to lock or unlock it, that thread will get an
error from the lock or unlock function.
THREAD 1 THREAD 2
pthread_mutex_lock (&a_mutex);
pthread_mutex_lock (&a_mutex);
a = data; /* blocked */
a++; /* blocked */
data = a; /* blocked */
pthread_mutex_unlock (&a_mutex); /* blocked */
b = data;
b--;
data = b;
pthread_mutex_unlock (&mut);
[data is fine. The data race is gone.]
threadadd.c
to produce the expected output, that is, the value 40.
Starvation and deadlock situations
Again we should remember that pthread_mutex_lock() might block
for a non-determined duration, in case of the mutex being already locked.
If it remains locked forever, it is said that our poor thread is "starved" -
it was trying to acquire a resource, but never got it. It is up to the
programmer to ensure that such starvation won't occur. The pthread library
does not help us with that.
The pthread library might, however, figure out a "deadlock". A deadlock is
a situation in which a set of threads are all waiting for resources taken by
other threads, all in the same set. Naturally, if all threads are blocked
waiting for a mutex, none of them will ever come back to life again. The
pthread library keeps track of such situations, and thus would fail the last
thread trying to call pthread_mutex_lock(), with an error
of type EDEADLK. The programmer should check for such a value,
and take steps to solve the deadlock somehow.
All POSIX semaphore functions and types are prototyped or defined in
semaphore.h. To define a semaphore object, use
sem_t sem_name;
To initialize a semaphore, use sem_init():
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_init(&sem_name, 0, 10);
int sem_wait(sem_t *sem);Example of use:
sem_wait(&sem_name);
int sem_post(sem_t *sem);Example of use:
sem_post(&sem_name);
int sem_getvalue(sem_t *sem, int *valp);
int value;
sem_getvalue(&sem_name, &value);
printf("The value of the semaphors is %d\n", value);
int sem_destroy(sem_t *sem);
sem_destroy(&sem_name);
Declare the semaphore global (outside of any funcion):
sem_t mutex;
Initialize the semaphore in the main function:
sem_init(&mutex, 0, 1);
THREAD 1 THREAD 2
sem_wait (&mutex);
sem_wait (&mutex);
a = data; /* blocked */
a++; /* blocked */
data = a; /* blocked */
sem_post (&mutex); /* blocked */
b = data;
b--;
data = b;
sem_post (&mutex);
[data is fine. The data race is gone.]
threadadd.c, so that
the program always produces the expected output (the value 40).
To compile a program that uses pthreads and posix semaphores, use
gcc -o filename filename.c -lpthread -lrt
Extend this code to implement a solution to the producer
consumer
problem discussed in class using Posix threads and semaphores. Assume
that there is only one producer and one consumer. The output of your code should be
similar to the following:
Producing A ...
Producing B ...
Producing C ...
Producing D ...
Consuming A ...
Consuming B ...
Consuming C ...
Consuming D ...
Producing E ...
Producing F ...
Producing G ...
Producing H ...
Consuming E ...
Consuming F ...
Consuming G ...
Consuming H ...
Producing I ...
Producing J ...
Consuming I ...
Consuming J ...
To compile a program that uses pthreads and posix semaphores, use
gcc -o filename filename.c -lpthread -lrt