DEV Community

Vivek Yadav
Vivek Yadav

Posted on

Mastering Multithreading in C Programming: A Deep Dive with In-Depth Explanations and Advanced Concepts

Introduction:

Multithreading in C programming enables developers to harness the full potential of modern multicore processors, facilitating concurrent execution of tasks within a single process. This comprehensive guide explores fundamental multithreading concepts, synchronization mechanisms, and advanced topics, providing detailed explanations and sample code for each concept.

1. Understanding Threads:

Threads are independent sequences of execution within a process, allowing for concurrent execution of tasks. Understanding thread creation, management, and states is crucial for effective multithreading.

Thread Creation:
pthread_create(): Initializes a new thread and starts its execution.
pthread_join(): Waits for a thread to terminate before proceeding.

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

void *threadFunc(void *arg) {
    printf("Hello from the new thread!\n");
    pthread_exit(NULL);
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, threadFunc, NULL);
    pthread_join(tid, NULL);
    printf("Back to the main thread.\n");
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

2. Synchronization and Mutual Exclusion:

Race conditions occur when multiple threads access shared resources concurrently, leading to unpredictable behavior. Synchronization mechanisms such as mutexes, semaphores, and condition variables ensure thread safety.

Mutexes (Mutual Exclusion):
Mutexes provide mutual exclusion, allowing only one thread to access a shared resource at a time. They prevent data corruption and ensure consistent behavior.

#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int sharedVariable = 0;

void *threadFunc(void *arg) {
    pthread_mutex_lock(&mutex);
    sharedVariable++;
    printf("Thread incremented sharedVariable to: %d\n", sharedVariable);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
    }

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, threadFunc, NULL);
    pthread_mutex_lock(&mutex);
    sharedVariable--;
    printf("Main thread decremented sharedVariable to: %d\n", sharedVariable);
    pthread_mutex_unlock(&mutex);
    pthread_join(tid, NULL);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Semaphores:
Semaphores are synchronization primitives used to control access to shared resources and coordinate the execution of multiple threads. They maintain a count to limit the number of threads accessing the resource simultaneously.

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

sem_t semaphore;

void *threadFunc(void *arg) {
    sem_wait(&semaphore);
    printf("Thread acquired semaphore\n");
    // Critical section
    sem_post(&semaphore);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid;
    sem_init(&semaphore, 0, 1); // Initialize semaphore with value 1
    pthread_create(&tid, NULL, threadFunc, NULL);
    // Main thread
    sem_wait(&semaphore);
    printf("Main thread acquired semaphore\n");
    // Critical section
    sem_post(&semaphore);
    pthread_join(tid, NULL);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

3. Thread Communication:

Thread communication facilitates coordination and synchronization between threads. Condition variables allow threads to wait for specific conditions to be met.

Condition Variables:
Condition variables enable threads to wait for a specific condition to occur. They are commonly used in producer-consumer scenarios, where a thread waits for data availability before proceeding.

#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t condVar = PTHREAD_COND_INITIALIZER;
int dataReady = 0;

void *producer(void *arg) {
    pthread_mutex_lock(&mutex);
    dataReady = 1;
    pthread_cond_signal(&condVar);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

void *consumer(void *arg) {
    pthread_mutex_lock(&mutex);
    while (!dataReady) {
        pthread_cond_wait(&condVar, &mutex);
    }
    printf("Consumer: Data is ready!\n");
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

int main() {
    pthread_t producerThread, consumerThread;
    pthread_create(&producerThread, NULL, producer, NULL);
    pthread_create(&consumerThread, NULL, consumer, NULL);
    pthread_join(producerThread, NULL);
    pthread_join(consumerThread, NULL);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

4. Advanced Concepts:

Advanced topics such as priority inversion, starvation, deadlock, and spinlock are critical for building robust multithreaded applications.

Priority Inversion:
Priority inversion occurs when a low-priority thread holds a resource required by a high-priority thread, causing priority inversion. Priority inheritance protocol helps mitigate this issue by temporarily raising the priority of the low-priority thread to that of the high-priority thread.

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

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void *highPriorityThread(void *arg) {
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    // Perform high-priority task
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    pthread_exit(NULL);
}

void *lowPriorityThread(void *arg) {
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);
    // Perform low-priority task
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    pthread_exit(NULL);
}

int main() {
    pthread_t highPrioTid, lowPrioTid;
    pthread_create(&highPrioTid, NULL, highPriorityThread, NULL);
    pthread_create(&lowPrioTid, NULL, lowPriorityThread, NULL);
    pthread_join(highPrioTid, NULL);
    pthread_join(lowPrioTid, NULL);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Starvation:
Starvation occurs when a thread is unable to gain access to required resources due to other threads continuously acquiring those resources. Fair scheduling policies ensure that all threads have a fair chance of resource allocation, preventing starvation.

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

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int sharedResource = 0;

void *threadFunc(void *arg) {
    pthread_mutex_lock(&mutex);
    // Increment shared resource
    sharedResource++;
    printf("Thread incremented sharedResource to: %d\n", sharedResource);
    pthread_mutex_unlock(&mutex);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid1, tid2;

    // Create two threads
    pthread_create(&tid1, NULL, threadFunc, NULL);
    pthread_create(&tid2, NULL, threadFunc, NULL);

    // Wait for both threads to finish
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    // Main thread
    pthread_mutex_lock(&mutex);
    // Access shared resource
    printf("Main thread accessed sharedResource: %d\n", sharedResource);
    pthread_mutex_unlock(&mutex);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Deadlock:
Deadlock occurs when two or more threads are waiting indefinitely for each other to release resources they need. Avoiding circular wait and implementing deadlock detection and recovery mechanisms help mitigate deadlock situations.

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

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void *thread1(void *arg) {
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    // Critical section
    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    pthread_exit(NULL);
}

void *thread2(void *arg) {
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);  // Potential deadlock point
    // Critical section
    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, thread1, NULL);
    pthread_create(&tid2, NULL, thread2, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Spinlock:
Spinlocks are synchronization primitives where a thread continuously polls for the availability of a resource. They are efficient for short critical sections and low contention scenarios.

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

pthread_spinlock_t spinlock;

void *threadFunc(void *arg) {
    pthread_spin_lock(&spinlock);
    // Critical section
    printf("Thread acquired spinlock\n");
    // Perform some task
    pthread_spin_unlock(&spinlock);
    pthread_exit(NULL);
}

int main() {
    pthread_t tid1, tid2;
    pthread_spin_init(&spinlock, 0);
    pthread_create(&tid1, NULL, threadFunc, NULL);
    pthread_create(&tid2, NULL, threadFunc, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_spin_destroy(&spinlock);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

Mastering multithreading in C programming requires a deep understanding of fundamental concepts, synchronization mechanisms, and advanced topics. By delving into these concepts and exploring sample code, developers can build robust, efficient, and responsive multithreaded applications. Continuous practice, experimentation, and adherence to best practices are key to becoming proficient in multithreading and developing reliable software systems that fully utilize the capabilities of modern hardware.

Top comments (3)

Collapse
 
pauljlucas profile image
Paul J. Lucas

C has had it's own thread support since C11 (13 years ago). There's no reason to use pthreads any more.

Collapse
 
vivekyadav200988 profile image
Vivek Yadav • Edited

AFAIK, std::thread is supported since C++11 but C doesn't have build in support for std::thread. On embedded platforms (UNIX based), pthread is the only option to leverage multithreading.

Please let me know if anyone disagree.

Collapse
 
pauljlucas profile image
Paul J. Lucas

I linked to a page describing standard thread support in C. Click the link!