SrcForge

Lesson 44

Multithreading (Pthreads)

Multithreading is the technique of running multiple threads within a single process. A thread is the smallest unit of execution in a program. In C programming on Unix-like systems, multithreading is implemented using the POSIX Threads (Pthreads) library. Threads share the same memory space, making communication fast but requiring careful synchronization using mutexes and condition variables to prevent race conditions.

C Track

Save progress as you learn.

44 lessons

Lesson content

Multithreading (Pthreads) in C Programming: A Complete Beginner to Advanced Guide

What is Multithreading in C Programming?

Modern computers have multiple CPU cores that can execute several tasks simultaneously. To take advantage of this hardware, applications often use multithreading. Instead of performing tasks one after another, a multithreaded program can execute multiple tasks concurrently, improving responsiveness and performance.

Multithreading is the technique of running multiple threads within a single process. A thread is the smallest unit of execution in a program. Unlike separate processes, threads share the same memory space, making communication between them fast and efficient. In C programming on Unix-like systems, multithreading is commonly implemented using the POSIX Threads (Pthreads) library.

Think of a process as a restaurant, and threads as the workers inside it. All workers share the same kitchen (memory), but each performs different tasks simultaneously.

Why Does Multithreading Exist?

Without multithreading, programs perform one task at a time, long-running operations can freeze applications, and multi-core processors are underutilized. With multithreading, multiple tasks execute concurrently, applications become more responsive, CPU resources are used more efficiently, and background processing becomes possible.

Real-World Use Cases

Multithreading is widely used in:

  • Web servers
  • Database systems
  • Video and image processing
  • Game engines
  • File compression utilities
  • Scientific computing
  • Embedded systems
  • Network servers
  • Web browsers
  • Operating systems

Prerequisites

Before learning Multithreading (Pthreads) in C programming, you should understand:

  • Basic C programming
  • Functions
  • Pointers
  • Structures
  • Dynamic memory allocation
  • Command-line compilation
  • Basic understanding of processes

Core Concepts

What is a Thread?

A thread is an independent execution path within a process. Each thread has its own program counter, stack, and registers. Threads in the same process share global variables, heap memory, open files, and process resources.

Process vs Thread

FeatureProcessThread
MemorySeparateShared
Creation CostHigherLower
CommunicationIPC requiredShared memory
ExecutionIndependentConcurrent
Context SwitchSlowerFaster

Pthreads Library

The POSIX Threads library provides functions for creating and managing threads. Include the header:

#include <pthread.h>
FunctionPurpose
`pthread_create()`Create a new thread
`pthread_join()`Wait for a thread to finish
`pthread_exit()`Terminate a thread
`pthread_self()`Get current thread ID
`pthread_equal()`Compare thread IDs

Creating a Thread

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

Joining Threads

pthread_join(thread, NULL);

Mutexes

A mutex (mutual exclusion) allows only one thread at a time to access a shared resource.

pthread_mutex_init()
pthread_mutex_lock()
pthread_mutex_unlock()
pthread_mutex_destroy()

Condition Variables

Condition variables allow threads to wait until a specific condition becomes true.

pthread_cond_wait()
pthread_cond_signal()
pthread_cond_broadcast()

Code Examples

Example 1: Beginner – Creating a Thread

#include <stdio.h>                  // Standard input/output library
#include <pthread.h>                // POSIX Threads library

void *printMessage(void *arg)       // Thread function
{
    printf("Hello from the thread!\n"); // Display message

    return NULL;                    // Exit thread
}

int main()                          // Program entry point
{
    pthread_t thread;               // Thread identifier

    pthread_create(&thread,         // Thread object
                   NULL,            // Default attributes
                   printMessage,    // Thread function
                   NULL);           // No argument

    pthread_join(thread, NULL);     // Wait for thread to finish

    return 0;                       // Exit successfully
}

Example 2: Intermediate – Passing Arguments to a Thread

#include <stdio.h>                  // Standard I/O library
#include <pthread.h>                // POSIX Threads library

void *square(void *arg)             // Thread function
{
    int number = *(int *)arg;       // Retrieve integer argument

    printf("Square = %d\n", number * number); // Compute square

    return NULL;                    // Exit thread
}

int main()                          // Program entry point
{
    pthread_t thread;               // Thread identifier

    int value = 5;                  // Integer to pass

    pthread_create(&thread,         // Create thread
                   NULL,            // Default attributes
                   square,          // Thread function
                   &value);         // Pass address of value

    pthread_join(thread, NULL);     // Wait for completion

    return 0;                       // Exit program
}

Example 3: Intermediate – Race Condition

#include <stdio.h>                  // Standard I/O library
#include <pthread.h>                // POSIX Threads library

int counter = 0;                    // Shared variable

void *increment(void *arg)          // Thread function
{
    for(int i = 0; i < 100000; i++) // Repeat many times
    {
        counter++;                  // Unsafe increment
    }

    return NULL;                    // Exit thread
}

int main()                          // Program entry point
{
    pthread_t t1, t2;               // Two thread identifiers

    pthread_create(&t1, NULL, increment, NULL); // First thread

    pthread_create(&t2, NULL, increment, NULL); // Second thread

    pthread_join(t1, NULL);         // Wait for first thread

    pthread_join(t2, NULL);         // Wait for second thread

    printf("%d\n", counter);        // Result is unpredictable

    return 0;                       // Exit program
}

Example 4: Advanced – Using a Mutex

#include <stdio.h>                  // Standard I/O library
#include <pthread.h>                // POSIX Threads library

int counter = 0;                    // Shared variable

pthread_mutex_t lock;               // Mutex object

void *increment(void *arg)          // Thread function
{
    for(int i = 0; i < 100000; i++) // Repeat many times
    {
        pthread_mutex_lock(&lock);  // Lock mutex

        counter++;                  // Safe increment

        pthread_mutex_unlock(&lock);// Unlock mutex
    }

    return NULL;                    // Exit thread
}

int main()                          // Program entry point
{
    pthread_t t1, t2;               // Thread identifiers

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

    pthread_create(&t1, NULL, increment, NULL); // First thread

    pthread_create(&t2, NULL, increment, NULL); // Second thread

    pthread_join(t1, NULL);         // Wait for first thread

    pthread_join(t2, NULL);         // Wait for second thread

    printf("%d\n", counter);        // Correct result

    pthread_mutex_destroy(&lock);   // Destroy mutex

    return 0;                       // Exit program
}

Example 5: Advanced – Thread Returning a Value

#include <stdio.h>                  // Standard I/O library
#include <stdlib.h>                 // Memory allocation
#include <pthread.h>                // POSIX Threads library

void *compute(void *arg)            // Thread function
{
    int *result = malloc(sizeof(int)); // Allocate memory

    *result = 100;                  // Store result

    return result;                  // Return pointer
}

int main()                          // Program entry point
{
    pthread_t thread;               // Thread identifier

    int *value;                     // Pointer for returned value

    pthread_create(&thread, NULL, compute, NULL); // Create thread

    pthread_join(thread, (void **)&value); // Retrieve result

    printf("%d\n", *value);         // Display result

    free(value);                    // Free allocated memory

    return 0;                       // Exit program
}

Common Mistakes and Pitfalls

WrongCorrect
Forgetting pthread_join()Wait for threads to finish
Ignoring return values of Pthread functionsCheck for errors
Accessing shared data without synchronizationUse mutexes or other synchronization primitives
Locking a mutex but never unlocking itAlways unlock in every execution path
Destroying a mutex while still in useDestroy only after all threads have finished

Wrong:

counter++;

Correct:

pthread_mutex_lock(&lock);

counter++;

pthread_mutex_unlock(&lock);

Best Practices

  • Compile with -pthread and enable compiler warnings.
  • Keep thread functions focused on a single task.
  • Minimize the amount of code executed while holding a mutex.
  • Protect every shared resource with appropriate synchronization.
  • Avoid global variables unless necessary.
  • Check the return values of all Pthread functions.
  • Use condition variables instead of busy waiting.
  • Prevent deadlocks by acquiring locks in a consistent order.
  • Clean up mutexes, condition variables, and other synchronization objects.
  • Test multithreaded programs under different workloads to uncover race conditions.

When NOT to Use This

Multithreading may not be the best solution when:

  • Your application performs only simple sequential tasks.
  • Thread creation overhead outweighs the performance benefit.
  • The workload is primarily I/O-bound and asynchronous I/O is more appropriate.
  • Shared data requires excessive synchronization, reducing concurrency.
  • Simpler process-based parallelism or event-driven programming better fits the problem.

Summary / Key Takeaways

  • Multithreading allows multiple threads to execute concurrently within the same process.
  • Threads share memory, making communication fast but requiring careful synchronization.
  • The Pthreads library provides APIs for creating, managing, and synchronizing threads.
  • pthread_create() starts a new thread, while pthread_join() waits for it to finish.
  • Race conditions occur when multiple threads access shared data without proper synchronization.
  • Mutexes protect shared resources and prevent data corruption.
  • Condition variables allow threads to wait efficiently for specific events.
  • Proper synchronization, error checking, and cleanup are essential for reliable multithreaded applications.

FAQ About Multithreading (Pthreads) in C

1. What is multithreading in C programming?

Multithreading is the execution of multiple threads within the same process, allowing several tasks to run concurrently while sharing the process's resources.

2. What is the Pthreads library?

Pthreads (POSIX Threads) is the standard threading library for Unix-like operating systems. It provides APIs for creating, managing, and synchronizing threads.

3. What is a race condition?

A race condition occurs when two or more threads access and modify shared data simultaneously without proper synchronization, leading to unpredictable results.

4. What is the purpose of a mutex?

A mutex (mutual exclusion) ensures that only one thread at a time can access a shared resource, preventing data corruption caused by concurrent access.

5. Why should I use pthread_join()?

pthread_join() waits for a thread to complete its execution. It ensures that resources are released correctly and that the main thread does not exit before worker threads finish.