Multithreading in C

Multithreading in C Language with Examples

In this article, I will discuss Multithreading in C Language with Examples. At the end of this article, you will understand the following pointers:

  1. What is Multithreading in C?
  2. How to Implement Multithreading in C Language?
  3. Implementing Multithreading in C Language
  4. Basic Thread Creation and Joining in C Using Pthread
  5. Using Mutex for Synchronization Example in C Pthreads
  6. Joining Threads Example in C Using Pthreads
  7. Thread Safety Example in C Using Pthreads
  8. Why Multithreading in C?
What is Multithreading in C?

Multithreading in C refers to a programming approach where multiple threads of execution run concurrently within a single process. This allows for parallel execution of code, improving the efficiency and performance of applications, particularly on multi-core processors. Since standard C does not have built-in support for multithreading, it relies on libraries or extensions to implement this functionality.

Important Terminology of Multithreading:
  • Threads: A thread is the smallest sequence of programmed instructions that a scheduler can manage independently. In multithreading, each thread represents a separate path of execution.
  • Concurrency: Multiple threads execute independently but share process resources like memory and file handles. This allows for the concurrent execution of tasks.
  • Parallelism: Multithreading can lead to true parallelism on multi-core systems where each thread can run simultaneously on a separate processor core.
  • Synchronization: Since threads share the same memory space, mechanisms to prevent conflicts and manage resource access are essential. This includes mutexes, semaphores, and critical sections.
  • Thread Safety: Code that functions correctly when accessed by multiple threads simultaneously is considered thread-safe. This often involves managing shared resources and avoiding race conditions.
How to Implement Multithreading in C Language?

Multithreading in the C language can be achieved using various libraries and APIs, as the standard C language itself does not include built-in support for multithreading. Here are the most commonly used approaches:

  • POSIX Threads (Pthreads): This is a widely used C library for creating and managing threads. Pthreads provide a set of functions for thread creation, synchronization, and management. It’s available on most Unix-like operating systems, including Linux and macOS.
  • Windows Threads: For programming on Windows, the native Windows API offers threading capabilities. Functions like CreateThread, ExitThread, WaitForSingleObject, etc., are used to manage threads in Windows applications.
  • C11 Standard Threads: In the C11 standard, a new thread library (<threads.h>) was introduced. It provides a simpler and more portable way to create and manage threads than Pthreads. However, support for this is not as widespread as Pthreads.
  • OpenMP (Open Multi-Processing): This is more of an extension to C/C++ and Fortran, which allows you to write parallel code using simple pragma directives. It’s very high-level compared to Pthreads and is often used for parallel processing in scientific computing.
  • Third-Party Libraries: There are also various third-party libraries like Boost.Thread (more common in C++) can be used in C projects to manage threading.
Key Concepts in Multithreading:
  • Thread Creation and Termination: Initiating new threads and properly terminating them after completing their task.
  • Synchronization: Mechanisms like mutexes, semaphores, and condition variables control the access to shared resources.
  • Deadlocks and Race Conditions: Understanding and preventing common threading issues like deadlocks (where two or more threads are waiting indefinitely for each other) and race conditions (where the outcome depends on the sequence or timing of thread execution).
  • Thread Safety: Writing code that functions correctly when accessed by multiple threads simultaneously.
  • Thread Local Storage: Data storage is visible only to a single thread.
Implementing Multithreading in C Language:

Multithreading in C can be implemented using various libraries, with the most common one being POSIX Threads, often referred to as Pthreads. Pthreads is a set of C programming language standards defined for thread creation and synchronization. Here’s a basic overview and some examples:

Understanding Pthreads in C
  • Thread Creation: A thread is created using the pthread_create() function, which takes four arguments. The first is a pointer to thread_id, which is set by this function. The second is attributes to the thread (you can pass NULL for default attributes). The third is the function name where the thread starts executing. The fourth is the argument to the function.
  • Thread Termination: A thread can terminate by returning from its start routine. It can also be terminated by another thread calling pthread_cancel().
  • Joining Threads: pthread_join() is used to wait for a thread to finish. When a thread calls this function, it will not continue until the thread it is waiting for has finished executing.
  • Mutexes for Synchronization: Mutexes are used to prevent data inconsistency due to race conditions. A mutex is locked using pthread_mutex_lock() before accessing shared data and unlocked using pthread_mutex_unlock() after accessing it.
Basic Thread Creation and Joining in C using Pthread

Creating and joining threads in C using the pthread library is straightforward. Here’s a basic example demonstrating how to create and join threads. This example involves creating two threads, each executing a separate function.

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

// Function prototypes for the thread routines
void *threadFunction1(void *arg);
void *threadFunction2(void *arg);

int main() {
    pthread_t thread1, thread2; // Thread identifiers
    int status1, status2;

    // Creating the first thread
    status1 = pthread_create(&thread1, NULL, threadFunction1, NULL);
    if (status1) {
        fprintf(stderr, "Error - pthread_create() return code: %d\n", status1);
        exit(EXIT_FAILURE);
    }

    // Creating the second thread
    status2 = pthread_create(&thread2, NULL, threadFunction2, NULL);
    if (status2) {
        fprintf(stderr, "Error - pthread_create() return code: %d\n", status2);
        exit(EXIT_FAILURE);
    }

    // Joining the first thread
    pthread_join(thread1, NULL);
    // Joining the second thread
    pthread_join(thread2, NULL);

    printf("Threads terminated, exiting the program\n");
    return 0;
}

void *threadFunction1(void *arg) {
    // Thread 1 performing some task
    printf("Thread 1 is running\n");
    // Do some work...
    pthread_exit(NULL);
}

void *threadFunction2(void *arg) {
    // Thread 2 performing some task
    printf("Thread 2 is running\n");
    // Do some work...
    pthread_exit(NULL);
}

When you run the above code, you will get the following output:

Multithreading in C Language with Examples

Explanation of the above Code:

Thread Creation: The pthread_create() function creates new threads. It takes four arguments:

  • A pointer to pthread_t, which uniquely identifies the thread.
  • A pointer to pthread_attr_t for thread attributes (NULL for default).
  • The function to be executed by the thread.
  • An argument to the thread function (NULL if not needed).

Error Checking: After each pthread_create(), checking the return value is good practice. A return value 0 indicates success, while a non-zero value indicates an error.

Thread Functions: threadFunction1 and threadFunction2 represent the tasks that each thread will execute.

Joining Threads: pthread_join() waits for a thread to terminate. It is called in the main function for each thread created. This ensures that the main program waits for all threads to complete their tasks before exiting.

Exiting Threads: Threads exit by returning from their start routine or by calling pthread_exit().

Using Mutex for Synchronization Example in C Pthreads

Using mutexes (short for mutual exclusion) is a key method for synchronizing access to shared resources in multithreaded programming with POSIX Threads (Pthreads) in C. Mutexes prevent multiple threads from accessing critical section code simultaneously, which helps in avoiding race conditions and ensuring thread safety. Here’s an example to illustrate the use of mutexes in a C program using Pthreads:

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

pthread_mutex_t lock; // Mutex lock
int sharedData = 0;   // Shared resource

// Function to be executed by threads
void* increment(void* arg) {
    pthread_mutex_lock(&lock); // Acquiring the mutex lock

    // Critical section begins
    sharedData++;
    printf("Thread %ld incremented sharedData to %d\n", (long)arg, sharedData);
    // Critical section ends

    pthread_mutex_unlock(&lock); // Releasing the mutex lock
    return NULL;
}

int main() {
    const int numberOfThreads = 5;
    pthread_t threads[numberOfThreads];
    pthread_mutex_init(&lock, NULL); // Initialize the mutex

    // Create multiple threads
    for (long i = 0; i < numberOfThreads; i++) {
        if (pthread_create(&threads[i], NULL, increment, (void*)i) != 0) {
            perror("Failed to create thread");
            return 1;
        }
    }

    // Joining the threads
    for (int i = 0; i < numberOfThreads; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("Failed to join thread");
            return 1;
        }
    }

    pthread_mutex_destroy(&lock); // Destroy the mutex

    printf("Final value of sharedData: %d\n", sharedData);
    return 0;
}

When you run the above code, you will get the following output:

Using Mutex for Synchronization Example in C Pthreads

Explanation of the above example:
  • Mutex Initialization: Before creating threads, the mutex is initialized using pthread_mutex_init.
  • Critical Section: In the increment function, the mutex is locked before accessing and modifying sharedData, a shared resource. After the modification, the mutex is unlocked. This ensures that only one thread at a time can modify sharedData.
  • Mutex Lock and Unlock: pthread_mutex_lock is used to acquire the lock, and pthread_mutex_unlock is used to release it. When a thread tries to acquire a lock already held by another thread, it will block until the lock becomes available.
  • Joining Threads: The main thread waits for all the other threads to finish executing by calling pthread_join.
  • Destroying the Mutex: After completing all the threads, the mutex is destroyed using pthread_mutex_destroy.
Joining Threads Example in C using Pthreads

Joining threads in C using the POSIX Threads (Pthreads) library is crucial to multithreaded programming. When you create a thread, it runs independently of the main thread. To ensure that the main thread waits for the other threads to finish their execution before it continues or terminates, you use the pthread_join function. Here’s an example to illustrate this:

Let’s assume we have a simple task that we want to execute in multiple threads, and then we want our main thread to wait for all these threads to complete.

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

// Thread function to be executed
void* task(void* arg) {
    long id = (long)arg;
    printf("Thread %ld is running\n", id);
    // Perform some operations here
    printf("Thread %ld finished its task\n", id);
    return NULL;
}

int main() {
    int numberOfThreads = 5;
    pthread_t threads[numberOfThreads];

    // Create multiple threads
    for (long i = 0; i < numberOfThreads; i++) {
        if (pthread_create(&threads[i], NULL, task, (void*)i) != 0) {
            perror("Failed to create thread");
            return 1;
        }
    }

    // Joining the threads
    for (int i = 0; i < numberOfThreads; i++) {
        if (pthread_join(threads[i], NULL) != 0) {
            perror("Failed to join thread");
            return 1;
        }
    }

    printf("All threads have finished.\n");
    return 0;
}

When you run the above code, you will get the following output:

Joining Threads Example in C using Pthreads

Explanation of the above Example:
  • Thread Creation: We define a task function, which is the code executed by each thread. In main, we create multiple threads using pthread_create, passing the task function and a unique ID for each thread.
  • Joining Threads: After creating the threads, the main thread calls pthread_join on each of the threads. pthread_join makes the main thread wait until the specified thread finishes its execution.
  • Thread Execution: Each thread runs the task function, which can be any code you want to execute in parallel. The thread prints its ID at the start and end of its execution.
  • Completion: Once all threads have been joined, the main thread prints a message indicating that all threads have finished.
Thread Safety Example in C using Pthreads

Thread safety is a critical aspect of multithreaded programming. It ensures that shared data is accessed and modified correctly when multiple threads operate concurrently. In C using Pthreads, thread safety can often be achieved through synchronization mechanisms like mutexes. Here’s an example to illustrate a thread-safe implementation:

In this example, we’ll create a simple counter incremented by multiple threads. We’ll use a mutex to ensure that the counter is incremented in a thread-safe manner.

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

#define NUM_THREADS 5

pthread_mutex_t mutex;
int counter = 0;

void* incrementCounter(void* threadid) {
    long tid;
    tid = (long)threadid;

    // Lock the mutex before accessing the shared resource
    pthread_mutex_lock(&mutex);
    
    counter++;
    printf("Thread %ld has incremented the counter. Counter value: %d\n", tid, counter);

    // Unlock the mutex once done
    pthread_mutex_unlock(&mutex);

    pthread_exit(NULL);
}

int main() {
    pthread_t threads[NUM_THREADS];
    int rc;
    long t;

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

    for(t = 0; t < NUM_THREADS; t++) {
        printf("In main: creating thread %ld\n", t);
        rc = pthread_create(&threads[t], NULL, incrementCounter, (void*)t);
        if (rc) {
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }

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

    printf("Final Counter Value: %d\n", counter);

    // Destroy the mutex
    pthread_mutex_destroy(&mutex);
    pthread_exit(NULL);
}

When you run the above code, you will get the following output:

Multithreading in C Language with Examples

Explanation of the above example:
  • Mutex Initialization: The mutex is initialized using pthread_mutex_init before creating threads.
  • Locking and Unlocking the Mutex: Each thread locks the mutex before modifying the shared counter variable and unlocks it after the modification. This ensures that only one thread at a time can modify the counter.
  • Thread Creation and Joining: The main function creates multiple threads executing the incrementCounter function. It then waits for all the threads to complete using pthread_join.
  • Mutex Destruction: After completing all threads, the mutex is destroyed using pthread_mutex_destroy.
Important Notes:

Proper error checking should be included in production code for functions like pthread_create, pthread_mutex_lock, pthread_mutex_unlock, and pthread_join. The above code is a simple example. Real-world scenarios might involve more complex operations and require careful design to ensure thread safety.

Key Points:
  • Always join or detach threads to prevent resource leaks.
  • Use mutexes or other synchronization methods to protect shared data.
  • Be mindful of the potential for deadlocks and race conditions.
  • Avoid excessive locking, which can lead to performance bottlenecks.
  • Error checking is essential but omitted here for brevity.
  • The behavior of threads depends on the operating system.
  • It’s crucial to properly manage resources (like mutexes) to avoid deadlocks or race conditions.
Challenges in Multithreading:
  • Complexity: Writing multithreaded code is inherently more complex than single-threaded code due to the need for synchronization and potential issues like deadlocks and race conditions.
  • Debugging: Debugging multithreaded applications can be challenging because of the non-deterministic nature of thread scheduling.
  • Resource Management: Proper management of resources like memory and file handles is crucial in a multithreaded environment to avoid leaks and data corruption.
Why Multithreading in C?

Multithreading in C offers several benefits, making it a popular choice in various programming scenarios:

  • Improved Application Performance and Responsiveness: By allowing multiple threads to run concurrently, applications can perform multiple operations simultaneously. This leads to better utilization of computing resources and can significantly improve the performance of applications, especially on multi-core processors.
  • Efficient Resource Sharing: Threads within the same process share memory and resources, which is more efficient than process-based parallelism, where each process has its own memory and resource allocation. This makes communication between threads faster and more efficient than inter-process communication.
  • Scalability: Multithreading enables applications to scale efficiently across multiple cores. As processors with more cores become commonplace, multithreaded applications can take advantage of the extra cores without significant changes to the code.
  • Improved User Experience in GUI Applications: In graphical user interface (GUI) applications, multithreading can prevent the user interface from becoming unresponsive while the application performs other tasks. For example, a background thread can handle time-consuming operations, while the main thread keeps the interface responsive to user actions.
  • Concurrency Control: Multithreading allows more control over prioritizing and executing tasks concurrently. This is particularly important in real-time applications or systems where tasks have specific timing requirements.
  • Cost-Effective: Utilizing threads can be more cost-effective than deploying multiple processes, as threads require less memory and resource allocation overhead.
  • Portability: The POSIX threads (Pthreads) library, commonly used for implementing multithreading in C, is widely supported across various operating systems. This makes it easier to write portable multithreaded C applications.
  • Suitability for Network and Server Applications: Multithreading is particularly useful in server and network applications. For instance, a web server can use separate threads to handle different client requests, allowing for concurrent processing and improved throughput.

In the next article, I will discuss Generic Selection in C Language. In this article, I explain Multithreading in C Language with Examples. I hope you enjoy this Multithreading in C Language with Examples article. I would like to have your feedback. Please post your feedback, questions, or comments about this article.

Leave a Reply

Your email address will not be published. Required fields are marked *