Introduction to POSIX Threads in C Programming

Understanding Threads and Their Advantages

In Unix-like systems, threads are often described as lightweight processes. While threads are not processes themselves, they can be considered cousins of Unix processes. Multiple threads within the same process share the following system resources:

  • Virtual address space
  • File descriptors
  • Signal handlers
  • Other process-level resources

However, each thread maintains its own:

  • Call stack
  • Register context (execution environment)
  • Thread-local storage

Threads enable applications to improve performance in multi-core environments, particularly when handling operations that might block, such as file I/O or socket communications.

When comparing multi-threading to multi-processing, the communication overhead differs significantly. In multi-process models, data exchange requires context switches between user space and kernel space. Multi-threading eliminates this overhead since threads can communicate through shared global variables.

Thread Lifecycle: Creation, Termination, and Joining

Creating Threads with pthread_create

The pthread_create function accepts four parameters:

  1. Thread identifier variable (output)
  2. Thread attributes pointer (NULL for defaults)
  3. Function pointer for the thread routine
  4. Argument to pass to the thread function
pthread_t thrd1;
pthread_attr_t *attr = NULL;
void *thread_function(void *argument);
char *some_argument = "Hello";

pthread_create(&thrd1, attr, thread_function, some_argument);

Terminating Threads with pthread_exit

pthread_exit(void *retval);

The retval parameter stores the thread's exit status that other threads can retrieve.

Waiting for Threads with pthread_join

After pthread_create succeeds, the execution order of the new thread and the calling thread is determined by the operating system's scheduler. When you need to wait for a specific thread to complete, use pthread_join—analogous to waitpid in process programming.

When thread A calls pthread_join on thread B, thread A blocks until thread B terminates. Upon B's exit, thread A collects B's return status.

Function parameters:

  • pthread_t th — identifier of the thread to wait for
  • void **thread_return — location to store the terminated thread's exit status
pthread_join(thrd1, NULL);

Complete Working Example

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

void *worker_routine(void *arg);

int main(void)
{
    int status1, status2;
    void *exit_status;
    pthread_t tid1, tid2;
    const char *msg1 = "Worker-A";
    const char *msg2 = "Worker-B";

    status1 = pthread_create(&tid1, NULL, worker_routine, (void *)msg1);
    status2 = pthread_create(&tid2, NULL, worker_routine, (void *)msg2);

    if (status1 != 0) {
        printf("Failed to spawn worker thread A\n");
    } else {
        printf("Worker thread A spawned\n");
    }

    if (status2 != 0) {
        printf("Failed to spawn worker thread B\n");
    } else {
        printf("Worker thread B spawned\n");
    }

    status1 = pthread_join(tid1, &exit_status);
    printf("Worker A join status: %d\n", status1);
    if (status1 != 0) {
        printf("Error joining worker A\n");
    }
    printf("Worker A terminated\n");

    status2 = pthread_join(tid2, &exit_status);
    printf("Worker B join status: %d\n", status2);
    if (status2 != 0) {
        printf("Error joining worker B\n");
    }
    printf("Worker B terminated\n");

    return 0;
}

void *worker_routine(void *arg)
{
    int counter;
    const char *label = (const char *)arg;

    for (counter = 0; counter < 5; counter++) {
        sleep(2);
        printf("%s iteration: %d\n", label, counter);
    }

    return NULL;
}

Compilation

gcc thread_demo.c -o demo -pthread

Note: The -pthread flag is mandatory. Omitting it produces linker errors:

undefined reference to `pthread_create`
undefined reference to `pthread_join`

Alternatively, -lpthread works aswell.

Sample Output

Worker thread A spawned
Worker thread B spawned
Worker-B iteration: 0
Worker-A iteration: 0
Worker-A iteration: 1
Worker-B iteration: 1
Worker-B iteration: 2
Worker-A iteration: 2
Worker-A iteration: 3
Worker-B iteration: 3
Worker-B iteration: 4
Worker-A iteration: 4
Worker A join status: 0
Worker A terminated
Worker B join status: 0
Worker B terminated

The interleaved output demonstrates that thread execution order is non-deterministic, controlled entirely by the system's thread scheduler.

Tags: c programming POSIX threads pthread multithreading Unix/Linux

Posted on Sat, 31 Oct 2026 16:12:39 +0000 by johndale