Getting Started with Threads: A Clear Guide to the Thread API

A clear guide to the POSIX thread API for creating, controlling, and coordinating threads.

🚦 Getting Started with Threads: A Clear Guide to the Thread API

In the previous blog post, we introduced the concept of concurrency and how threads enable multiple points of execution within a single program. Now, let’s explore how we actually create, control, and coordinate threads using a practical API—specifically, POSIX threads (often called pthreads). We’ll also touch upon common pitfalls and best practices for robust thread management.

Creating a Thread in POSIX

Creating threads in C using pthreads is straightforward. The essential function to know is pthread_create:

#include <pthread.h>

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

Here’s what the arguments mean in plain English:

  • thread: A pointer to a thread identifier that lets you interact with this new thread later.
  • attr: Optional attributes for the thread (e.g., stack size or scheduling priority). Usually, default attributes (NULL) suffice.
  • start_routine: A pointer to the function the thread should run.
  • arg: A single argument to pass to that thread function.

An example of a simple thread creation:

#include <stdio.h>
#include <pthread.h>

typedef struct {
    int a;
    int b;
} myarg_t;

void *mythread(void *arg) {
    myarg_t *args = (myarg_t *)arg;
    printf("Arguments: %d and %d\n", args->a, args->b);
    return NULL;
}

int main() {
    pthread_t p;
    myarg_t args = {10, 20};

    pthread_create(&p, NULL, mythread, &args);
    pthread_join(p, NULL);

    return 0;
}

In this example, mythread receives structured arguments packed into a single argument (myarg_t). Once created, the new thread immediately begins executing alongside the main thread.

Waiting for Threads to Complete: Joining Threads

Sometimes you need to wait for a thread to finish execution. To do this, use pthread_join:

int pthread_join(pthread_t thread, void **value_ptr);
  • The first argument specifies which thread to wait for.
  • The second is a pointer to receive the thread’s return value.

Here’s a complete example demonstrating how threads can return values safely:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

typedef struct { int x; int y; } myret_t;

void *mythread(void *arg) {
    myret_t *result = malloc(sizeof(myret_t));
    result->x = 1;
    result->y = 2;
    return (void *) result;
}

int main() {
    pthread_t p;
    myret_t *result;

    pthread_create(&p, NULL, mythread, NULL);
    pthread_join(p, (void **) &result);

    printf("Returned values: %d and %d\n", result->x, result->y);
    free(result);

    return 0;
}

Important caution: Never return a pointer to a local (stack-allocated) variable from a thread. Once the thread returns, its stack is gone, leaving a dangling pointer, leading to unexpected behavior or crashes.

Protecting Shared Data: Locks

When threads share data, we must protect this data using locks to prevent race conditions. The pthread API provides a simple and effective locking mechanism through mutexes:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_lock(&lock);
    // Critical section: safe access to shared data
pthread_mutex_unlock(&lock);

Important: Locks must always be initialized before use—either statically (PTHREAD_MUTEX_INITIALIZER) or dynamically via pthread_mutex_init.

For dynamic initialization:

pthread_mutex_init(&lock, NULL);
// ... use lock ...
pthread_mutex_destroy(&lock);

Always check for errors returned by mutex functions to ensure correctness:

int rc = pthread_mutex_lock(&lock);
if (rc != 0) {
    // handle error
}

Wrapping mutex calls can simplify error handling:

void safe_mutex_lock(pthread_mutex_t *mutex) {
    int rc = pthread_mutex_lock(mutex);
    assert(rc == 0);
}

Signaling Between Threads: Condition Variables

Condition variables enable threads to signal each other about events or state changes:

  • pthread_cond_wait() puts the thread to sleep until notified.
  • pthread_cond_signal() wakes one waiting thread.

Here’s an illustrative usage of condition variables:

Waiting thread:

pthread_mutex_lock(&lock);
while (ready == 0)
    pthread_cond_wait(&cond, &lock);
pthread_mutex_unlock(&lock);

Signaling thread:

pthread_mutex_lock(&lock);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);

Two essential points:

  • Condition variables always require an associated mutex.
  • Use a loop (while) to check the condition rather than an if to guard against spurious wake-ups.

Important advice: Don’t attempt to signal between threads using a simple flag alone (like repeatedly checking a boolean value). Such approaches lead to subtle bugs and poor performance. Always prefer condition variables and associated mutexes.

Compiling and Running Threaded Programs

To compile programs that use pthreads, ensure to include pthread.h and use the -pthread compiler flag:

gcc -o main main.c -Wall -pthread

Including these explicitly ensures your compiler links your program against the pthread library properly.

Best Practices in Threaded Programming

Here’s a quick summary of tips for robust threaded programming:

  • Keep it simple: Complex interactions between threads lead to bugs.
  • Minimize interactions: Reduce complexity by limiting how threads interact.
  • Always initialize mutexes and conditions: Uninitialized variables cause unpredictable failures.
  • Check return codes: Ignored errors lead to obscure and difficult-to-debug problems.
  • Avoid passing stack pointers across threads: Always allocate shared data on the heap.
  • Use condition variables instead of flags: Flags cause bugs and performance issues; condition variables are safer and clearer.
  • Consult documentation: The POSIX pthread documentation (man pthread) is detailed and helpful.

Wrapping Up

The pthread API gives us a practical way to handle concurrency, from creating threads to safely sharing data and synchronizing operations. The real challenge isn’t learning these API calls—it’s applying them correctly and carefully in your programs.

Concurrency opens powerful possibilities, but only if handled with precision. By mastering these basic pthread concepts, you set yourself up for writing efficient, safe, and robust multithreaded code.

As always, happy coding!