Introduction to PThreads

Introduction to PThreads

In our last article, I introduced the concepts of threads.

We learned what threads are and how do they speed up our code.

You can check out the previous article here:

What are threads?
Most, if not all modern operating systems support multiprocessing and at the same time processes can be run in multiple threads hence the word multithreading. This will give you a significant increase in performance and scalability but it comes with a price. The overall complexity of the code incre…

Today we will take a different route and talk about how to implement multithreading in C using the pthread library.

What are Pthreads?

Source: https://slideplayer.com/slide/5312609/

In the past, hardware vendors implemented their own versions of threads.

This means that companies such as IBM, Apple, and Intel all had their own proprietary versions of threads.

These implementations differed substantially from each other making it difficult for programmers to develop portable threaded applications.

To fix this a standardized programming interface was made.

  • For UNIX systems, this interface has been specified by the IEEE POSIX 1003.1c standard (1995).
  • Implementations adhering to this standard are referred to as POSIX threads or Pthreads.
  • Most hardware vendors now offer Pthreads in addition to their proprietary API’s.

Over the years the POSIX standard has continued to evolve and undergo revisions, including the Pthreads specifications.

So coming back to our question what are Pthreads?

Pthreads is a C/C++ library used to manage threads that are based on the POSIX standard.

The methods in the Pthreads library can be categorized into four groups:

  • Thread Management –  Routines that work directly on threads - creating, detaching, joining, etc. They also include functions to set/query thread attributes (joinable, scheduling etc.)
  • Mutexes –  Routines that deal with synchronization, called a “mutex”, which is an abbreviation for “mutual exclusion”. Mutex functions provide for creating, destroying, locking and unlocking mutexes. These are supplemented by mutex attribute functions that set or modify attributes associated with mutexes.
  • Condition variables –  Routines that address communications between threads that share a mutex. Based upon programmer specified conditions. This group includes functions to create, destroy, wait and signal based upon specified variable values. Functions to set/query condition variable attributes are also included.
  • Synchronization –  Routines that manage read/write locks and barriers.

Next, let's see how we can use the Pthread library to play around with threads.

PThreads in Action

Before we look at the specific methods that Pthreads provide us.

Let's first look at the types that are provided:

  • pthread_ – Threads themselves and miscellaneous methods.
  • pthread_t— Identifier for a thread.
  • pthread_mutex_t- Mutex (Mutual exclusion) synchronization primitive.
  • pthread_mutexattr_t— Mutex creation attribute.
  • pthread_cond_t— Condition Variable synchronization primitive.
  • pthread_condattr_t— Condition variable creation attribute.
  • pthread_key_t— Thread local storage key.
  • pthread_once_t— Once time initialization control variable.
  • pthread_attr_t— Thread creation attribute.

You can read more about the types that Pthreads provides here:

<sys/types.h>

Disclaimer

PThreads provides many different methods for thread management but unfortunately, we won't be able to cover them all. We will only cover the basic methods so that you can get started creating multithreaded programs.  

You can check out the list of methods that Pthreads provides here:

<pthread.h>

Creating Threads

Initially, your main() program comprises a single, default thread. All other threads must be explicitly created by the programmer. pthread_create creates a new thread and makes it executable. This method can be called any number of times from anywhere within your code.

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

It has four arguments:

  • thread –  An opaque, unique identifier for the new thread returned by the subroutine.
  • attr –  An opaque attribute object that may be used to set thread attributes. You can specify a thread attributes object or NULL for the default values.
  • start_routine –  the C function that the thread will execute once it is created.
  • arg –  A single argument that may be passed to start_routine. It must be passed by reference as a pointer cast of type void. NULL may be used if no argument is to be passed.

Upon successful completion pthread_create()returns code 0, a non-zero value signals an error.

PS. The maximum number of threads that may be created by a process is implementation-dependent. Programs that attempt to exceed the limit can fail or produce wrong results.

Another important note is that once a thread is created, it can create other threads.

There is no implied hierarchy or dependency between threads.

Here's a basic example of a multithreaded program:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
  
// A normal C function that is executed as a thread 
// when its name is specified in pthread_create()
void *helloWorld(void *vargp)
{
    sleep(1);
    printf("Hello World \n");
    return NULL;
}
   
int main()
{
    pthread_t thread_id;
    printf("Before Thread\n");
    pthread_create(&thread_id, NULL, helloWorld, NULL);
    pthread_join(thread_id, NULL);
    printf("After Thread\n");
    exit(0);
}

Terminating Threads

There are several ways in which a thread may be terminated:

  • The thread returns normally from its starting routine. Its work is done.
  • The thread makes a call to the pthread_exit method - whether its work is done or not.
  • The thread is cancelled by another thread via the pthread_cancel routine.
  • The entire process is terminated due to making a call to either the exec() or exit().
  • If main() finishes first, without calling pthread_exit explicitly itself then all other threads will abruptly end.

For this section, we will take a look at the pthread_exit method.

void pthread_exit(void *retval);

Here's a basic example of a multithreaded program with pthread_exit.

#include <pthread.h>
#include <stdio.h>
#define NUM_THREADS 5

void *PrintHello(void *threadid)
{
    long tid;
    tid = (long)threadid;
    printf("Hello World! It's me, thread #%ld!\n", tid);
    pthread_exit(NULL);
}

int main (int argc, char *argv[])
{
    pthread_t threads[NUM_THREADS];
    int rc;
    long t;
    for(t=0; t<NUM_THREADS; t++){
       printf("In main: creating thread %ld\n", t);
       rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t);
       if (rc){
          printf("ERROR; return code from pthread_create() is %d\n", rc);
          exit(-1);
       }
    }

    /* Last thing that main() should do */
    pthread_exit(NULL);
}

Joining Threads

“Joining” is one way to accomplish synchronization between threads. For example:

Joining is done by the pthread_join method.

What it essentially does is that it waits till the given thread ends.

If the thread was already completed before the pthread_join method was called then the method will immediately return a value.

int pthread_join (pthread_t THREAD_ID, void ** DATA);

Here's a basic example of a multithreaded program with pthread_join.

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS	4

void *BusyWork(void *t)
{
   int i;
   long tid;
   double result=0.0;
   tid = (long)t;
   printf("Thread %ld starting...\n",tid);
   for (i=0; i<1000000; i++)
   {
      result = result + sin(i) * tan(i);
   }
   printf("Thread %ld done. Result = %e\n",tid, result);
   pthread_exit((void*) t);
}

int main (int argc, char *argv[])
{
   pthread_t thread[NUM_THREADS];
   pthread_attr_t attr;
   int rc;
   long t;
   void *status;

   /* Initialize and set thread detached attribute */
   pthread_attr_init(&attr);
   pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

   for(t=0; t<NUM_THREADS; t++) {
      printf("Main: creating thread %ld\n", t);
      rc = pthread_create(&thread[t], &attr, BusyWork, (void *)t); 
      if (rc) {
         printf("ERROR; return code from pthread_create() is %d\n", rc);
         exit(-1);
         }
      }

   /* Free attribute and wait for the other threads */
   pthread_attr_destroy(&attr);
   for(t=0; t<NUM_THREADS; t++) {
      rc = pthread_join(thread[t], &status);
      if (rc) {
         printf("ERROR; return code from pthread_join() is %d\n", rc);
         exit(-1);
         }
      printf("Main: completed join with thread %ld having a status of %ld\n",t,(long)status);
      }
 
printf("Main: program completed. Exiting.\n");
pthread_exit(NULL);
}

Terminating Threads Early

Sometimes you just want to end a thread early.

You can do it using the pthread_cancel method.

int pthread_cancel (pthread_t THREAD_ID);

It is important to understand that although pthread_cancel()returns immediately and can terminate a thread prematurely, it cannot be called a means of forcing threads to terminate.

So essentially pthread_cancel sends a cancellation request but if you want to delete a thread, you must make sure to use pthread_join beforehand.

Here's a small code snippet on how pthread_cancel is used.

pthread_t tid;

pthread_create(&tid, 0, worker, NULL);

pthread_cancel(tid);

Detaching Threads

Illustration of joining/detaching threads in C++

If you want a thread to run independently of the main thread then you can use the pthread_detach method.

int pthread_detach(pthread_t thread);

But beware this is more of a death sentence.

Detached threads cannot be joined and you cannot undo the detachment.

Conclusion

The topic of threads is almost bottomless but I hope I've covered the introductions here.

I might've mistaken somewhere as I'm not an expert in systems programming. If I did feel free to criticise me on social media.  

Thanks for reading.

Member discussion

-->