Multi-thread
programming with POSIX threads
ECE382M.20,
Fall 2014
Seogoo Lee <sglee@utexas.edu>
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
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?
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.
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.
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.