Operating Systems Lab: Critical Section,
Mutexes, and Semaphores
Learning Outcomes
- Understand the concept of a critical section.
- Explore race conditions through a simple example.
- Apply mutexes and semaphores to synchronize access.
- Compare different synchronization primitives.
Part 1: Illustrative Task — Race Condition and Mutex
Task: Observe a Race Condition and Solve it with a Mutex
Instructions:
1. Write a program that creates two threads.
2. Each thread increments a shared global counter 1,000,000 times.
3. First, run the code without any synchronization.
4. Then, add a pthread_mutex_t to protect the increment operation.
Starter Code (Without Mutex):
#include <stdio.h>
#include <pthread.h>
long counter = 0;
void* increment(void* arg) {
for (long i = 0; i < 1000000; i++) {
counter++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter value: %ld\n", counter);
return 0;
}
Functions to be used
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);
Modified Code (With Mutex):
#include <stdio.h>
#include <pthread.h>
long counter = 0;
void* increment(void* arg) {
for (long i = 0; i < 1000000; i++) {
counter++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter value: %ld\n", counter);
return 0;
}
Part 2: Challenge Task — Binary Semaphore for Synchronization
Task: update the following code to use binary semaphore
Instructions:
1. Modify your mutex-based code to use POSIX unnamed semaphores (sem_t).
2. Initialize a semaphore to 1 (binary semaphore).
3. Use sem_wait() before and sem_post() after the increment operation.
4. Verify that synchronization still works using semaphore instead of mutex.
Functions to be used
sem_t sem;
sem_init(&sem, 0, 1);
sem_wait(&sem);
sem_post(&sem);
Code Using Semaphore:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
long counter = 0;
void* increment(void* arg) {
for (long i = 0; i < 1000000; i++) {
counter++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem);
printf("Final counter value: %ld\n", counter);
return 0;
}
Reflection Questions
1. What is the difference between a mutex and a binary semaphore?
2. Which synchronization primitive is more appropriate for mutual exclusion and why?
3. Can you identify scenarios where a counting semaphore is more useful than a mutex?
Part 3: Additional Task — Process Synchronization Using Semaphore
Task: Use Named Semaphore for Synchronization Between Processes
Instructions:
1. Create a program that forks into two processes.
2. Each process should increment a shared memory counter 1,000 times.
3. Use a POSIX named semaphore (`sem_open`, `sem_wait`, `sem_post`) to protect access to
the shared counter.
4. Use `mmap` or `shm_open` to create shared memory for the counter.
5. After both processes finish, print the final value.
Functions to be used:
sem_t *sem = sem_open("/mysem", O_CREAT, 0644, 1);
sem_wait(sem);
sem_post(sem);
sem_close(sem);
sem_unlink("/mysem");
Example Code (Process-based with Named Semaphore):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/wait.h>
int main() {
int *counter = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*counter = 0;
pid_t pid = fork();
if (pid == 0) { // Child process
for (int i = 0; i < 1000; i++) {
(*counter)++;
}
exit(0);
} else {
for (int i = 0; i < 1000; i++) {
(*counter)++;
}
wait(NULL);
printf("Final counter value: %d\n", *counter);
munmap(counter, sizeof(int));
}
return 0;
}