Multi-thread programming with POSIX threads

ECE382M.20, Fall 2014

Seogoo Lee <sglee@utexas.edu>

 


1       Overview

Two basic methods to utilize multiple CPU cores are to implement multiple processes or threads. Considering large overhead in using multiple processes, however, we focus on multiple threads in this document. POSIX library (Pthread) is one of the methods to implement multi-threaded applications. To properly use Pthread library, you need to understand the following items.

         Basic functions of Pthread library

         Synchronization among threads

 

Several useful web resources are:

         POSIX Thread - Wikipedia

         POSIX Threads Programming - Lawrence Livermore National Laboratory

         POSIX Thread Library Tutorial – YoLinux.com


1       Basic usage 

The most important functions of Pthread library are pthread_create and pthread_join. Let's start from a simple example C code (pthread_ex1.c) that does not have function arguments and dependencies. Later in this instruction, we will explore more complicated examples with multiple arguments, C++ classes (Pthread is a C library, so we need a trick to use it with C++ classes.), and dependency handling.

 

#include <stdio.h>     

#include <stdlib.h>    

#include <pthread.h>    // POSIX Threads

 

using namespace std;

 

// prototype for thread routine

void *print_message_function1 ( void *ptr );

void *print_message_function2 ( void *ptr );

 

int main()

{

    pthread_t thread1, thread2;  // thread variables

   

    // create threads 1 and 2

    pthread_create (&thread1, NULL, print_message_function1, NULL);

    pthread_create (&thread2, NULL, print_message_function2, NULL);

 

    // Main block now waits for both threads to terminate, before it exits

    pthread_join(thread1, NULL);

    pthread_join(thread2, NULL);

 

    // exit

    exit(0);

} // main()

 

// Thread1 function

void *print_message_function1 ( void *ptr )

{

    thdata *data;           

   

    // do the work

    int i;

    for (i=0;i<10;i++) {

      printf("Thread %d says %s \n", 1, "Hello");

    }

    pthread_exit(0);

}

 

// Thread2 function

void *print_message_function2 ( void *ptr )

{

    thdata *data;           

 

    // do the work

    int i;

    for (i=0;i<10;i++) {

      printf("Thread %d says %s \n", 2, "World");

    }

    pthread_exit(0);

}

 

Let's compile this example code using 'g++ -pthread pthread_ex1.c', and run the executable. In the code, we create two threads (So, we have three parallel threads including the main thread.) If you run the executable several times, you will see the two threads are running concurrently and the order in printing 10 'Hello's and 10 'World's can be different at every run.

 

At first, we generated a thread handler by pthread_t, and create a thread with the handler by pthread_create (e.g. pthread_create (&thread1, NULL, print_message_function1, NULL);). Here, the function running on the thread is print_message_function1., which is often called as start routine. Since this function has no argument, the 4th argument of pthread_create is NULL, i.e. we pass required arguments for a start routine via the 4th argument of pthread_create.

 

Two start routines, print_message_function1 and print_message_function2, do their own task, and go back to the main thread by pthread_exit. An important thing to note here is that a thread is still alive even after execution of the start routine has finished and the program has gone back to the parent thread (the main thread in this example). For this reason, we need to manually terminate all threads before the main thread ends.

 

pthread_join does this task. When a parent thread that creates a child thread meets pthread_join, it waits until the child thread's execution finishes, and safely terminates the child thread. Please note that the main thread is also running in parallel with the child threads it created.

 

The prototype of a thread function is 'void *start_routine ( void *ptr);'. The return type is 'void *', and there exists only one argument with 'void *' type. So, the next question is: how can we pass multiple arguments to a start routine?

 


2       Passing Arguments to a Start Routine

If a start routine needs more than one argument, we can define a new type that contains all the required arguments. Here is an example (pthread_ex2.c).

 

#include <stdio.h>     

#include <stdlib.h>    

#include <pthread.h>   

#include <string.h>    

 

using namespace std;

 

// prototype for thread routine

void *print_message_function1 ( void *ptr );

void *print_message_function2 ( void *ptr );

 

// to pass multiple arguments

typedef struct str_thdata

{

    int thread_no;

    char message[100];

} thdata;

 

int main()

{

    pthread_t thread1, thread2; 

    thdata data1, data2;        

   

    // initialize data to pass to thread 1

    data1.thread_no = 1;

    strcpy(data1.message, "Hello");

 

    // initialize data to pass to thread 2

    data2.thread_no = 2;

    strcpy(data2.message, "World");

   

    // create threads 1 and 2

    pthread_create (&thread1, NULL, print_message_function1, (void *) &data1);

    pthread_create (&thread2, NULL, print_message_function2, (void *) &data2);

 

    pthread_join(thread1, NULL);

    pthread_join(thread2, NULL);

             

    exit(0);

}

 

void *print_message_function1 ( void *ptr )

{

    thdata *data;           

    data = (thdata *) ptr;  // type cast to a pointer to thdata

   

    int i;

    for (i=0;i<10;i++) {

      printf("Thread %d says %s \n", data->thread_no, data->message);

    }

    pthread_exit(0);

}

 

void *print_message_function2 ( void *ptr )

{

    thdata *data;           

    data = (thdata *) ptr;  // type cast to a pointer to thdata

 

    int i;

    for (i=0;i<10;i++) {

      printf("Thread %d says %s \n", data->thread_no, data->message);

    }

    pthread_exit(0);   

}

 

 In this example, the start routines need two arguments, thread_no and message. We define a new structured type thdata that contains these two arguments. And we declare a variable with the new type (e.g. thdata data1;), assign values to the arguments (e.g. data1.thread_no = 1;), and pass the new variable to thread_create by  casting it to 'void *' type. In the start routine that uses these arguments, we cast the 'void *' type argument back to the newly-defined thdata type (e.g. data = (thdata *) ptr;), and use the pointer variable.


3       Working with C++ Classes 

One thing to note is that Pthread is a C library, not C++ library. Therefore, pthread_create cannot directly call a class member function as a start routine. We have a workaround for this limitation using a helper function and class. Here is an example (pthread_ex3.c).

 

#include <stdio.h>     

#include <stdlib.h>    

#include <pthread.h>   

#include <string.h>    

 

using namespace std;

 

// to pass multiple arguments

typedef struct str_thdata

{

    int thread_no;

    char message[100];

} thdata;

 

// Thread functions are member functions of this class

class print_message {

  public:

    print_message () {}

    ~print_message () {}

 

    void *print_message_function1 ( void *ptr )

    {

        thdata *data;           

        data = (thdata *) ptr;  // type cast to a pointer to thdata

       

        int i;

        for (i=0;i<10;i++) {

          printf("Thread %d says %s \n", data->thread_no, data->message);

        }

        pthread_exit(0);

    }  

 

    void *print_message_function2 ( void *ptr )

    {

        thdata *data;           

        data = (thdata *) ptr;  // type cast to a pointer to thdata

 

        int i;

        for (i=0;i<10;i++) {

          printf("Thread %d says %s \n", data->thread_no, data->message);

        }

        pthread_exit(0);   

    }

};

 

// This is a helper class for thread1 function

class thread_launcher1 {

  public:

    thread_launcher1(print_message *obj, void *arg) :

      p_obj(obj), p_arg(arg) {}

    void *launch () { p_obj->print_message_function1(p_arg); }

    print_message *p_obj;

    void *p_arg;

};

 

// This is a helper class for thread2 function

class thread_launcher2 {

  public:

    thread_launcher2(print_message *obj, void *arg) :

      p_obj(obj), p_arg(arg) {}

    void *launch () { p_obj->print_message_function2(p_arg); }

    print_message *p_obj;

    void *p_arg;

};

 

// This is a helper function for thread1 function

void *launch_member_function1(void *obj) {

  thread_launcher1 *mylauncher = reinterpret_cast<thread_launcher1 *> (obj);

  mylauncher->launch();

}

   

// This is a helper function for thread2 function

void *launch_member_function2(void *obj) {

  thread_launcher2 *mylauncher = reinterpret_cast<thread_launcher2 *> (obj);

  mylauncher->launch();

}

 

int main()

{

    pthread_t thread1, thread2; 

    thdata data1, data2;        

   

    // initialize data to pass to thread 1

    data1.thread_no = 1;

    strcpy(data1.message, "Hello");

 

    // initialize data to pass to thread 2

    data2.thread_no = 2;

    strcpy(data2.message, "World");

   

    // a class that has thread functions as member functions

    print_message thread_func;

 

    // helper classes to call thread functions

    thread_launcher1 launcher1 (&thread_func, (void *) &data1);

    thread_launcher2 launcher2 (&thread_func, (void *) &data2);

 

    // create threads 1 and 2

    pthread_create (&thread1, NULL, launch_member_function1, (void *) &launcher1);

    pthread_create (&thread2, NULL, launch_member_function2, (void *) &launcher2);

 

    pthread_join(thread1, NULL);

    pthread_join(thread2, NULL);

             

    exit(0);

}

 

In this example, the start routines, print_message_function1 and print_message_function2, are member functions of print_message class, and we cannot directly call them from pthread_create. Therefore, we first define a helper function which is not a member function of any classes and works as a new start routine. Now, the helper function is called instead of the real start routine that is a member function of a class. The helper functions in the above example are launch_member_function1, and launch_member_function2. A problem is that a helper function needs two items: the real arguments to be passed to the real start routine (data1 for thread1 in the above example) and the pointer of the class that has the real start routine as a member function. However, again, Pthread_create only accepts only one argument for its start routine, and we cannot pass these two items to the helper function directly. In the above example, this problem is resolved by introducing a helper class that has two pointer variables p_obj and p_arg (e.g. thread_launcher1). p_obj is the pointer of the class that has the real start routine, and p_arg is the argument pointer for the thread function. Now we pass the pointer of the helper class to pthread_create as the argument.    

 


4       Synchronization among Multiple Threads

Parallel processing always goes with synchronization. Here, we have a simple example that introduces mutex of Pthread (pthread_ex4.c), which can be useful for synchronization of multiple threads that have shared resources.

 

#include <stdio.h>     

#include <stdlib.h>    

#include <pthread.h>   

#include <string.h>    

 

using namespace std;

 

// prototype for thread routine

void *print_message_function1 ( void *ptr );

void *print_message_function2 ( void *ptr );

 

// struct to hold data to be passed to a thread: now it has a mutex pointer

typedef struct str_thdata

{

    int thread_no;

    int shared_buf[1000];

    int buf_size;

    pthread_mutex_t *mutex;

} thdata;

 

int main()

{

    pthread_t thread1, thread2; 

    thdata data;

 

    // mutex

    pthread_mutex_t mutex;

    pthread_mutex_init(&mutex, NULL);

 

    // initialize data to pass to thread 1

    data.thread_no = 1;

    data.buf_size = 0;

    data.mutex = &mutex;

 

    // create threads 1 and 2

    pthread_create (&thread1, NULL, print_message_function1, (void *) &data);

    pthread_create (&thread2, NULL, print_message_function2, (void *) &data);

 

    pthread_join(thread1, NULL);

    pthread_join(thread2, NULL);

             

    exit(0);

}

 

void *print_message_function1 ( void *ptr )

{

    thdata *data;           

    data = (thdata *) ptr;  // type cast to a pointer to thdata

    pthread_mutex_t *mutex = data->mutex;

 

    int i;

    int cnt = 0;

    while (cnt < 999) {

      for (i=cnt;i<cnt+50;i++) {

        pthread_mutex_lock (mutex);

        printf("Thread %d says %s %d \n", data->thread_no, "Hello", i);

        data->shared_buf[i]=i;

        data->buf_size += 1;

        pthread_mutex_unlock (mutex);

      }

      cnt += 50;

    }

   

    pthread_exit(0);

}

 

void *print_message_function2 ( void *ptr )

{

    thdata *data;           

    data = (thdata *) ptr;  // type cast to a pointer to thdata

    int cnt = 0;   

    int k;

    pthread_mutex_t *mutex = data->mutex;

 

    while (cnt < 999) {

      if (data->buf_size >= 100) {

        pthread_mutex_lock (mutex);

        printf ("buf_size: %d \n", data->buf_size);

        for (k=cnt;k<cnt+100;k++) {

          printf("Thread %d says %s %d %d \n", data->thread_no, "Hi", k, data->shared_buf[k]);

        }

        data->buf_size -= 100;

        cnt += 100;

        pthread_mutex_unlock (mutex);

      }

    }

 

    pthread_exit(0);

}

 

In this example, two threads are using a shared buffer. One thread (thread1) writes data to the shared buffer, and the other (thread2) reads the data from the buffer. thread2 monitors the buffer, and when there exist more than one hundred unread data entries in the buffer, it reads one hundred entries in order. What we want to achieve by using mutex is to protect the reading operation, i.e. while thread2 is in reading operation, thread1 stops writing a new entry to the buffer and waits until thread2 finishes its job. This functionality can be implemented with mutex. Once thread2 starts its reading operation, it blocks the other threads by pthread_mutex_lock(mutex). After finishing its operation, it unlocks the mutex by pthread_mutex_unlock(mutex) so that the other threads can start working on their own task again. Here, one thing to note is that a programmer should know all threads that access shared variables, and put pthread_mutex_lock and pthread_mutex_unlock in all of them. More information about mutex is available in the linked web resources.