Skip to content

C Multithreading — POSIX Threads (pthreads) for Concurrent Programming

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about C Multithreading. We cover key concepts, practical examples, and best practices to help you master this topic.

C multithreading using POSIX threads (pthreads) enables concurrent execution with thread creation (pthread_create), synchronization (mutexes, condition variables), and thread management (join, detach) for parallel processing on multi-core systems.

What You Will Learn

  • Creating threads with pthread_create and joining with pthread_join
  • Protecting shared data with mutexes (pthread_mutex_t)
  • Signaling between threads with condition variables
  • Thread-local storage with __thread
  • Avoiding deadlocks and race conditions
  • Thread safety of standard library functions

Why It Matters

Modern CPUs have multiple cores that sit idle if your program is single-threaded. Multithreading lets you Process data in parallel, handle multiple clients simultaneously, and keep the UI responsive while doing background work. POSIX threads are the standard threading API on Unix-like systems (Linux, macOS, BSD). Durga Antivirus Pro uses a thread pool to scan multiple files concurrently, with one thread per CPU core, reducing full-system scan time from hours to minutes.

Real-World Use

A web server creates a new thread for each incoming connection. While one thread serves a slow client downloading a large file, another thread handles a quick API request from a different client. Without threads, the fast request would wait for the slow download to complete.

Learning Path

flowchart LR
  A[Signals] --> B[Multithreading\nYou are here]
  B --> C[Network Sockets]
  style B fill:#f90,color:#fff

Creating and Joining Threads

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

void* print_numbers(void *arg) {
    int id = *(int*)arg;
    for (int i = 1; i <= 5; i++) {
        printf("Thread %d: %d\n", id, i);
        usleep(100000);  // 100ms
    }
    return NULL;
}

int main() {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;

    // Create two threads
    if (pthread_create(&t1, NULL, print_numbers, &id1) != 0) {
        perror("Failed to create thread 1");
        return 1;
    }
    if (pthread_create(&t2, NULL, print_numbers, &id2) != 0) {
        perror("Failed to create thread 2");
        return 1;
    }

    // Wait for both threads to complete
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("Both threads completed.\n");
    return 0;
}

Compile with -pthread:

gcc -pthread -o threads threads.c
./threads

Output (interleaved, may vary):

Thread 1: 1
Thread 2: 1
Thread 1: 2
Thread 2: 2
Thread 1: 3
Thread 2: 3
...

Returning Values from Threads

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

typedef struct {
    int start;
    int end;
    int result;
} Range;

void* sum_range(void *arg) {
    Range *r = (Range*)arg;
    r->result = 0;
    for (int i = r->start; i <= r->end; i++) {
        r->result += i;
    }
    return NULL;
}

int main() {
    Range r1 = {1, 50000000, 0};
    Range r2 = {50000001, 100000000, 0};
    pthread_t t1, t2;

    pthread_create(&t1, NULL, sum_range, &r1);
    pthread_create(&t2, NULL, sum_range, &r2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    long long total = (long long)r1.result + r2.result;
    printf("Sum 1..100000000 = %lld\n", total);

    return 0;
}

Mutex Synchronization

A mutex prevents multiple threads from accessing shared data simultaneously:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;

void* increment(void *arg) {
    int id = *(int*)arg;

    for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        shared_counter++;
        pthread_mutex_unlock(&mutex);
    }

    printf("Thread %d done\n", id);
    return NULL;
}

int main() {
    pthread_t threads[5];
    int ids[5];

    for (int i = 0; i < 5; i++) {
        ids[i] = i + 1;
        pthread_create(&threads[i], NULL, increment, &ids[i]);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(threads[i], NULL);
    }

    printf("Final counter: %d (expected: 500000)\n", shared_counter);

    pthread_mutex_destroy(&mutex);
    return 0;
}

Without the mutex, the counter would be less than 500,000 due to race conditions.

Condition Variables

Condition variables let threads wait for a specific condition:

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
int data = 0;

void* producer(void *arg) {
    for (int i = 1; i <= 5; i++) {
        sleep(1);

        pthread_mutex_lock(&mutex);
        data = i * 100;
        ready = 1;
        printf("Produced: %d\n", data);
        pthread_cond_signal(&cond);  // Wake up one consumer
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

void* consumer(void *arg) {
    for (int i = 0; i < 5; i++) {
        pthread_mutex_lock(&mutex);

        // Wait while condition is false (spurious wakeup safe)
        while (!ready) {
            pthread_cond_wait(&cond, &mutex);
        }

        printf("Consumed: %d\n", data);
        ready = 0;
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main() {
    pthread_t prod, cons;

    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);

    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

Thread-Local Storage

Each thread gets its own copy of a variable declared with __thread:

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

// Each thread has its own copy of this variable
__thread int thread_local_counter = 0;

void* worker(void *arg) {
    int id = *(int*)arg;

    thread_local_counter = id * 100;
    printf("Thread %d: thread_local_counter = %d\n", id, thread_local_counter);

    // Each thread modifies only its own copy
    thread_local_counter += 50;
    printf("Thread %d: after increment = %d\n", id, thread_local_counter);

    return NULL;
}

int main() {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;

    pthread_create(&t1, NULL, worker, &id1);
    pthread_create(&t2, NULL, worker, &id2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

Thread Pool Pattern

A simple thread pool for parallel task execution:

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

#define NUM_THREADS 4
#define NUM_TASKS 20

pthread_mutex_t task_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t task_cond = PTHREAD_COND_INITIALIZER;
int next_task = 0;
int completed_tasks = 0;

void* thread_worker(void *arg) {
    int id = *(int*)arg;

    while (1) {
        pthread_mutex_lock(&task_mutex);

        // Wait if no tasks, but check if all done
        while (next_task >= NUM_TASKS && completed_tasks < NUM_TASKS) {
            pthread_cond_wait(&task_cond, &task_mutex);
        }

        if (completed_tasks >= NUM_TASKS) {
            pthread_mutex_unlock(&task_mutex);
            break;
        }

        int task = next_task++;
        pthread_mutex_unlock(&task_mutex);

        // Process task
        printf("Thread %d processing task %d\n", id, task);
        usleep(100000 + rand() % 200000);

        pthread_mutex_lock(&task_mutex);
        completed_tasks++;
        if (completed_tasks == NUM_TASKS) {
            pthread_cond_broadcast(&task_cond);  // Wake all waiting threads
        }
        pthread_mutex_unlock(&task_mutex);
    }

    printf("Thread %d exiting\n", id);
    return NULL;
}

int main() {
    pthread_t threads[NUM_THREADS];
    int ids[NUM_THREADS];

    for (int i = 0; i < NUM_THREADS; i++) {
        ids[i] = i;
        pthread_create(&threads[i], NULL, thread_worker, &ids[i]);
    }

    // Wake up threads to start processing
    pthread_cond_broadcast(&task_cond);

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

    printf("All %d tasks completed.\n", completed_tasks);
    return 0;
}

Common Mistakes

  1. Race conditions from missing mutexes: Two threads reading/writing the same variable without synchronization causes unpredictable results. Always protect shared mutable state with a mutex.

  2. Deadlock from multiple mutexes: If thread A locks mutex1 then mutex2, and thread B locks mutex2 then mutex1, they deadlock. Always lock mutexes in the same order.

  3. Forgetting to unlock on error paths: If a function locks a mutex and returns early on error without unlocking, the mutex is never released. Use a goto cleanup pattern or restructure the code.

  4. Spurious wakeups from condition variables: pthread_cond_wait can return even if the condition is not signaled. Always check the condition in a while loop, not if.

  5. Calling pthread_join on a detached thread: Detached threads cannot be joined. Their resources are automatically reclaimed when they exit. Joining a detached thread returns an error.

  6. Not compiling with -pthread: The pthreads library requires linking with -pthread. Without it, you get undefined reference errors.

  7. Assuming thread-safe library functions: Most C library functions are not thread-safe by default. Functions like strtok, asctime, and rand use static internal state. Use their _r variants (strtok_r, rand_r) in threaded code.

Practice Questions

  1. What is a Race Condition and how does a mutex prevent it?
  2. Why must pthread_cond_wait be used inside a while loop instead of an if statement?
  3. What is a deadlock and how can it be avoided?
  4. How does thread-local storage differ from a global variable protected by a mutex?
  5. Challenge: Implement a parallel merge sort using pthreads. Split the array in half, sort each half in a separate thread, then merge the results. Add a depth limit so threads are not created for very small sub-arrays (use sequential sort below a threshold).

Mini Project

Build a parallel file search tool:

  • The program takes a directory path and a filename pattern
  • It recursively scans the directory tree, collecting file paths
  • It distributes the file paths across a thread pool (one thread per CPU core)
  • Each thread checks if its assigned files match the pattern (using fnmatch or strstr)
  • Matching files are added to a shared result list protected by a mutex
  • The main thread prints results as they come in (but no faster than one per line)
  • Measure the speedup compared to a single-threaded version
  • Handle errors: permission denied, symlink loops, invalid path

FAQ

How many threads should I create?

Typically one per CPU core for CPU-bound tasks. For I/O-bound tasks, more threads can help by overlapping I/O waits. Creating thousands of threads wastes memory (each thread needs ~8 MB stack).

What is the difference between pthread_join and pthread_detach?

pthread_join blocks until the thread exits and returns its result. pthread_detach marks the thread's resources for automatic reclamation when it exits -- no join needed.

Can I kill a thread from another thread?

pthread_cancel sends a cancellation request but does not forcibly kill the thread. The target thread must reach a cancellation point. There is no safe way to forcibly kill a thread in POSIX.

What is the difference between a mutex and a semaphore?

A mutex allows one thread at a time to access a resource (binary). A semaphore allows N threads to access N identical resources (counting). Use mutexes for exclusive access, semaphores for resource pools.

How do I debug multithreaded programs?

Use tools like Helgrind (Valgrind), ThreadSanitizer (compiler flag -fsanitize=thread), and GDB's thread commands (info threads, thread N, bt). These detect race conditions and deadlocks.

What is Next

Proceed to Network Sockets to learn about TCP/IP socket programming. Then explore I/O Multiplexing for handling multiple connections with select/poll/epoll.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C