A repository of exercises to support the training.
In this module, you will explore multithreading concepts, focusing on race conditions and synchronization mechanisms like mutexes and semaphores. The exercises will help you understand the problems caused by unsynchronized threads and provide techniques to solve these issues using various thread synchronization primitives.
In this exercise, you will observe a race condition in a sample code and then solve the issue by implementing a mutex.
Step 1: Observe Race Conditions
When two or more threads access shared data simultaneously without proper synchronization, a race condition can occur. Run the following code and observe inconsistent results due to the race condition.
Sample Code:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
#define NUM_ITERATIONS 1000000
int counter = 0; // Shared resource (global variable)
void* incrementCounter(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
counter++; // Race condition occurs here
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
pthread_create(&threads[i], NULL, incrementCounter, NULL);
}
// Join threads
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Final Counter Value: %d\n", counter); // The result will be inconsistent
return 0;
}
Step 2: Implement Mutex to Solve the Race Condition Now, implement a mutex to ensure that the increment operation on the shared resource (counter) is protected and executed atomically by only one thread at a time. Observe how the race condition is resolved, and the final value of the counter remains consistent.
Example Code (Race Condition Fixed with Mutex)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define NUM_THREADS 5
#define NUM_ITERATIONS 1000000
int counter = 0; // Shared resource (global variable)
pthread_mutex_t mutex; // Mutex declaration
void* incrementCounter(void* arg) {
for (int i = 0; i < NUM_ITERATIONS; i++) {
// Lock the mutex before modifying the counter
counter++;
// Unlock the mutex after modification
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
// Initialize the mutex
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
}
// Join threads
for (int i = 0; i < NUM_THREADS; i++) {
}
printf("Final Counter Value: %d\n", counter); // The result will now be consistent
// Destroy the mutex
return 0;
}
In this exercise, you will use semaphores to control access to a shared resource among multiple threads. A semaphore is a signaling mechanism that limits the number of threads that can access a shared resource at any time. In this case, learners will observe a fixed number of threads allowed to access a resource simultaneously. Run the example code below and observe how only 3 threads are allowed to access the shared resource at any given time. Modify the semaphore's initial value and observe how it changes the number of threads accessing the resource simultaneously.
Example code:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM_THREADS 10
sem_t semaphore; // Semaphore declaration
void* accessSharedResource(void* arg) {
int thread_id = *((int*)arg);
printf("Thread %d is waiting to access the shared resource...\n", thread_id);
// Wait on the semaphore (decrements the semaphore value)
sem_wait(&semaphore);
// Critical section (accessing the shared resource)
printf("Thread %d is accessing the shared resource.\n", thread_id);
sleep(1); // Simulate work being done on the shared resource
// Signal on the semaphore (increments the semaphore value)
sem_post(&semaphore);
printf("Thread %d is done accessing the shared resource.\n", thread_id);
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];
// Initialize the semaphore with an initial value of 3 (allowing 3 threads to access the resource at a time)
sem_init(&semaphore, 0, 3);
// Create threads
for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, accessSharedResource, &thread_ids[i]);
}
// Join threads
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
// Destroy the semaphore
sem_destroy(&semaphore);
return 0;
}
Next Steps:
Stay curious and keep experimenting!