Class 29 of 37 • System Programming (C) \t\t\t\t

POSIX Threads (pthreads) in Linux C Programming

In modern computer architecture with multi-core CPUs, multithreading enables high-performance parallel execution. In Linux and POSIX-compliant operating systems, the standard C multithreading API is defined by the POSIX Threads (pthreads) library in the <pthread.h> header.

Linux POSIX Threads Memory Model Diagram

Figure 1: Single-Threaded vs Multi-Threaded Process: Shared Heap, Data, & Text vs Private Stacks & Registers.

Process vs Thread in Linux

Under the Linux kernel, threads are implemented as Lightweight Processes (LWPs) created via the clone() system call with flags sharing memory space:

Feature

Heavyweight Process (fork)

Lightweight Thread (pthreads)

Memory Space

Completely isolated address space (Copy-on-Write).

Shares Code, Data, Heap, and File Descriptors.

Private Resources

PCB, PID, Page Tables, File Descriptors, Heap, Stack.

Thread ID (TID), Execution Stack, CPU Registers, Program Counter.

Creation Cost

High CPU & RAM overhead (duplicating page tables).

Very low overhead (up to 30x faster than fork).

Communication

Requires IPC (Pipes, FIFOs, Shared Memory, Sockets).

Direct reading/writing of shared memory & global variables.

Core Pthread API Functions

Function

Purpose

pthread_create()

Spawns a new concurrent thread executing a specified start routine function.

pthread_join()

Suspends calling thread until the target thread terminates (analogous to wait() for processes).

pthread_exit()

Terminates calling thread and returns a void pointer status to a joining thread.

pthread_self()

Returns the unique Thread ID (pthread_t) of the calling thread.

pthread_detach()

Separates thread so its resources are automatically reclaimed immediately upon exit without joining.

Compilable Example 1: Creating Concurrent Threads in C

The program below creates two worker threads that execute simultaneously in background:

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

// Thread worker function signature must be: void* (*func)(void*)
void* print_message(void* ptr) {
    char* message = (char*) ptr;
    for (int i = 1; i <= 3; i++) {
        printf("[%s] Running iteration %d (TID: %lu)\n", message, i, (unsigned long)pthread_self());
        sleep(1);
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t thread1, thread2;
    char msg1[] = "Worker-Alpha";
    char msg2[] = "Worker-Beta";

    printf("[MAIN] Spawning concurrent threads...\n");

    // Syntax: pthread_create(&thread_id, attributes, function_pointer, argument_pointer)
    if (pthread_create(&thread1, NULL, print_message, (void*)msg1) != 0) {
        perror("Failed to create thread 1");
        return 1;
    }

    if (pthread_create(&thread2, NULL, print_message, (void*)msg2) != 0) {
        perror("Failed to create thread 2");
        return 2;
    }

    // Wait for both threads to complete
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    printf("[MAIN] Both worker threads joined successfully. Program exiting.\n");
    return 0;
}

Compiling Pthread Programs in Linux

$ gcc -Wall -O2 thread_demo.c -pthread -o thread_demo
$ ./thread_demo

The Concurrency Danger: Race Conditions

Because threads share the same global variables and heap memory, simultaneous write operations can interleave on the CPU registers, causing Race Conditions. For instance, if two threads increment counter++ 100,000 times without synchronization, the final result will be far less than 200,000 due to lost updates!

Thread Synchronization with Mutex Locks

A Mutex (Mutual Exclusion) lock acts as an electronic key to a Critical Section. Only the single thread holding the lock can enter; all other threads are blocked until the lock is released:

Mutex Function

Operation

pthread_mutex_init(&lock, NULL)

Initializes mutex lock with default attributes.

pthread_mutex_lock(&lock)

Acquires lock. If already locked, thread sleeps until lock is released.

pthread_mutex_unlock(&lock)

Releases lock, waking up any waiting blocked threads.

pthread_mutex_destroy(&lock)

Destroys lock object and frees internal synchronization resources.

Compilable Example 2: Thread-Safe Banking Balance Deposit using Mutex

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

#define NUM_THREADS 4
#define TRANSACTIONS_PER_THREAD 100000

// Shared resource
long account_balance = 0;

// Mutex lock object
pthread_mutex_t balance_lock;

void* deposit_money(void* arg) {
    for (int i = 0; i < TRANSACTIONS_PER_THREAD; i++) {
        // --- CRITICAL SECTION BEGIN ---
        pthread_mutex_lock(&balance_lock);
        
        account_balance += 10; // Atomically safe deposit of $10

        pthread_mutex_unlock(&balance_lock);
        // --- CRITICAL SECTION END ---
    }
    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];

    // Initialize mutex
    pthread_mutex_init(&balance_lock, NULL);

    printf("[MAIN] Starting %d teller threads...\n", NUM_THREADS);

    // Create 4 threads depositing money concurrently
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_create(&threads[i], NULL, deposit_money, NULL);
    }

    // Join all threads
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // Expected: 4 * 100,000 * 10 = $4,000,000
    printf("[MAIN] All transactions finished!\n");
    printf("[MAIN] Final Account Balance: $%ld (Expected: $4000000)\n", account_balance);

    // Destroy mutex
    pthread_mutex_destroy(&balance_lock);
    return 0;
}

Lab Practice: Parallel Array Sum using Multithreading in C

Problem: Write a C program that computes the sum of a 1,000,000-element integer array divided equally across 4 threads using pthreads.

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

#define SIZE 1000000
#define NUM_THREADS 4

int numbers[SIZE];
long partial_sums[NUM_THREADS];

typedef struct {
    int thread_id;
    int start_index;
    int end_index;
} ThreadData;

void* sum_slice(void* arg) {
    ThreadData* data = (ThreadData*) arg;
    long local_sum = 0;

    for (int i = data->start_index; i < data->end_index; i++) {
        local_sum += numbers[i];
    }

    partial_sums[data->thread_id] = local_sum;
    printf("[Thread %d] Summed indices %d to %d: partial_sum = %ld\n",
           data->thread_id, data->start_index, data->end_index, local_sum);

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    ThreadData thread_data[NUM_THREADS];
    int slice_size = SIZE / NUM_THREADS;

    // Initialize array with 1s (Expected total sum = 1,000,000)
    for (int i = 0; i < SIZE; i++) {
        numbers[i] = 1;
    }

    // Launch threads
    for (int t = 0; t < NUM_THREADS; t++) {
        thread_data[t].thread_id = t;
        thread_data[t].start_index = t * slice_size;
        thread_data[t].end_index = (t + 1) * slice_size;
        pthread_create(&threads[t], NULL, sum_slice, (void*)&thread_data[t]);
    }

    // Wait for all worker threads
    for (int t = 0; t < NUM_THREADS; t++) {
        pthread_join(threads[t], NULL);
    }

    // Aggregate results
    long total_sum = 0;
    for (int t = 0; t < NUM_THREADS; t++) {
        total_sum += partial_sums[t];
    }

    printf("==========================================\n");
    printf("Total Parallel Array Sum: %ld (Expected: %d)\n", total_sum, SIZE);
    printf("==========================================\n");
    return 0;
}

Frequently Asked Questions (FAQs)

A process has its own isolated virtual address space and file descriptor table. A thread (LWP) shares the code, global data, heap, and open files with other threads in the same process, having only its own private stack and CPU registers.
The -pthread flag links the POSIX thread library (libpthread) and enables thread-safe reentrant standard library definitions (such as _REENTRANT).
A terminated non-detached thread retains its exit state and stack allocation until joined, creating a thread leak similar to a zombie process. If joining is not required, use pthread_detach(pthread_self()).
A deadlock occurs when two threads are blocked forever waiting for locks held by each other (e.g. Thread 1 holds Lock A and waits for Lock B, while Thread 2 holds Lock B and waits for Lock A). Deadlocks are prevented by acquiring multiple locks in a strict global hierarchy.

Next Step in Your Linux Learning Path:

Build client-server TCP/IP network communication engines in Class 30: Linux Socket Programming.

Continue to Class 30: Linux Socket Programming → Previous: ← Signals | Full Syllabus

Share Share on Facebook Share on Twitter Share on LinkedIn Pin on Pinterest Share on Stumbleupon Share on Tumblr Share on Reddit Share on Diggit

You may also like this!