Multithreaded programming

Difference between revision 12 and current revision

No diff available.

What is a thread?

A process in Unix is actually a running program with an address space. This space contains the program itself, the program's data and its stack. This process could be seen as a glass jar. At the bottom of the jar lays some food (the resources) and in the jar, a fly is doing things (the fly is a thread).

A multithreaded application could be seen as the same jar, but with more than one fly; the flies share the resources of the jar. Each fly can talk to the other and they can also wave to flies in other jars. Thus essentially, a thread is a program counter, a stack and a set of registers. Note that a process can be seen as a single-threaded application.

Compiler issues

On Linux, you should compile and link with option -pthread. This will automatically include any necessary flags to let glibc know that we're multithreading.

Create a thread with pthread_create()

 int pthread_create(pthread_t* thread, pthread_attr_t* attr, 
                    void* (*start_routine)(void*), void* arg);

This function creates a thread. The thread begins to execute the function pointed to by the third parameter start_routine. Arguments for the function can be passed through the fourth parameter void* arg. That leaves the second parameter, which passes arguments for the thread, see also pthread_attr_init(). The following example passes NULL for both thread and function arguments. This means using the default settings.

When the main program finishes, all threads are cancelled as well. This is discussed later.

 #include <pthread.h>
 #include <stdlib.h>
 #include <stdio.h>
 #include <unistd.h>
 void reader_function(char*);
 int main(void)
 {
       int n_check;        /* a generally used variable to check for errors */
       pthread_t reader;              /* the identifier for our thread */
       char msg[] = "Hello, world!";
       /* this thread will do the reader function */
       n_check = pthread_create( &reader, NULL,
                       (void*)&reader_function, (void*)msg);
       /* wait for reader thread to terminate */
       sleep(1);
       return 0;
 }
 void reader_function(char* msg)
 {
       printf("I am the reader thread, my message is:%s\n", msg);
       pthread_exit(0);
 }

If you don't need parameters, pass NULL as the fourth parameter of pthread_create(). If you need multiple parameters, define a struct containing all parameters you need.

Waiting for thread completion with pthread_join()

In the previous example, we waited for thread completion using sleep(). This is a bad way! We're counting that the reader thread gets CPU time when the main thread sleeps and that doesn't have to be the case if the CPU is very busy with other processes. Better is to join the thread.

The syntax of the function:

 int pthread_join(pthread_t target_thread, void** thread_return)

This function blocks until the target thread is done and grabs the return value. This blocking action is called: "joining a thread". To succeed joining, target_thread has to be joinable. A thread is joinable when it is created with default attributes. Joinable means, that a thread its file descriptor and stack will not be cleaned up after exiting. These resources will only be reallocated after a join by another thread.

The opposite of a joinable thread is a detached thread. A detached thread can be created by passing the right attributes. Or by calling pthread_detach() on a joinable thread. When a detached thread exits, its resources are available for reuse. But no synchronisation is possible on a detached thread. If the main program exits, all detached threads are stopped too.

The scheme above shows that joinable threads at one point synchronize with the main thread. And that detached threads do not return something to the main thread.

Blocking until another thread has done something

The example below consists of a main program, which kicks off a thread. The main program then waits until the thread tells all other threads (including main) that it has done something. We call this a condition variable. It's a bit of a confusing term, since it's not actually used as a variable containing something useful for the application.

WARNING this code is not finished yet, please don't use

 #include <stdio.h>
 #include <stdlib.h>
 #include <pthread.h>
 #include <unistd.h>
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
 pthread_cond_t cond;
 int value;
 void *notif_th();
 int main(void)
 {
     pthread_t pt_notif;
     pthread_cond_init(&cond, NULL);
     pthread_create(&pt_notif, NULL, notif_th, NULL);
     // Wait for cond to be changed
     printf("Waiting for the conditional...\n");
     pthread_cond_wait(&cond, &mutex);
     printf("Conditional received, joining...\n");
     pthread_join(pt_notif, NULL);
     return 0;
 }

WARNING this code is not finished yet, please don't use

 /*
  * This thread sleeps for 5 seconds, then sets the conditional to 1.
  */
 void *notif_th()
 {
     printf("Thread sleeps a bit so main can set up cond_wait\n");
     sleep(1);
     printf("Thread sends signal\n");
     pthread_cond_signal(&cond);
     printf("Thread sleeps a bit afterwards\n");
     sleep(5);
     return;
 }

WARNING this code is not finished yet, please don't use

The output should be something like:

 Waiting for the conditional...
 Thread sleeps a bit so main can set up cond_wait
 Thread sends signal
 Conditional received, joining...
 Thread sleeps a bit afterwards

Using mutexes

A nice example on how to use mutexes as binary semaphores is shown in the following example. For the theoretical explanation, see this Wikipedia entry.

 #include <stdio.h>
 #include <stdlib.h>
 #include <pthread.h>
 #include <unistd.h>
 
 pthread_mutex_t full = PTHREAD_MUTEX_INITIALIZER;
 pthread_mutex_t empty = PTHREAD_MUTEX_INITIALIZER;
 
 int shared_val = 0;
 int quit = 0;
 
 void notif_th();
 
 /*
  * This thread is the consumer and will decrease the variable
  * "shared_val" whenever it's increased.
  */
 int main(void)
 {
     int i = 0;
     pthread_t pt_notif;
     pthread_mutex_lock(&full);
 
     pthread_create(&pt_notif, NULL, (void*) notif_th, NULL);
 
     while(i < 10) {
         pthread_mutex_lock(&full);
         shared_val--;
         printf("Main (consumer) sets var to %d\n", shared_val);
         pthread_mutex_unlock(&empty);
         i++;
     }
     quit=1;
     printf("Main joining...\n");
     pthread_join(pt_notif, NULL);
 
     return 0;
 }
 
 /*
  * This thread is the producer and increases a variable.
  */
 void notif_th()
 {
     while(!quit) {
         pthread_mutex_lock(&empty);
         shared_val++;
         printf("Thread (producer) sets var to %d\n", shared_val);
         pthread_mutex_unlock(&full);
     }
     return;
 }

Thread attributes: the pthread_attr_*() functions

Threads can be tailored for your needs. First, create a variable of type pthread_attr_t. After you set it to the right settings, pass pthread_attr_t to the function pthread_create().

Several functions are involved when dealing with variables of type pthread_attr_t. First, the function pthread_attr_init() should be called. This sets all attributes to default. Then, attributes can be set and viewed with a set and get function for each attribute. Those functions all have a name starting with pthread_attr_* where * is get or set.

The attributes roughly take care of two things: joinable/detached and scheduling. On the subject of joinable/detached, see the explanation of pthread_join(). If scheduling is important for you, check your man pages.

Example of setting attributes.

 void reader_function(void);
 int main(void)
 {
       int            n_check; /* generally used variable for checking errors */
       pthread_t      reader;                /* the identifier for our thread */
       pthread_attr_t reader_attr;    /* the attributes for the reader thread */
       /* initialize the attributes. Set to a realtime round-robin scheduling
          policy. Make the thread compete with all the processes requiring
          CPU resources, not just competition between threads in this process.
          Finally, make the thread a detached one, see pthread_join()         */
       pthread_attr_init(&reader_attr);
       pthread_attr_setschedpolicy(&reader_attr, SCHED_RR);
       pthread_attr_setscope(&reader_attr, PTHREAD_SCOPE_SYSTEM);
       pthread_attr_setdetachstate(&reader_attr, PTHREAD_CREATE_DETACHED);
       /* this thread will do the reader function */
       n_check = pthread_create( &reader, &reader_attr,
                                (void*)&reader_function, NULL);
       if (n_check != 0)
       {
             /* error handling */
       }
       .
       .
       .
       /* wait for reader thread to terminate */
       return 0;
 }
 void reader_function(void)
 {
       printf("I am the reader thread!");
       pthread_exit(0);
 }