Locks and semaphores are two of the most commonly used synchronization primitives in parallel and concurrent programming. Both are used to manage access to shared resources, preventing race conditions, ensuring mutual exclusion, and allowing synchronization between threads or processes. While they are conceptually similar, they are used in slightly different ways and offer different levels of control.
Let’s break down locks and semaphores, understand their differences, and explore their usage in programming.
A lock is a synchronization mechanism used to enforce mutual exclusion (mutex) by ensuring that only one thread or process can access a critical section of code at a time. When a thread locks a resource, no other thread can access it until the lock is released.
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t lock; // Declare a mutex
void* thread_func(void* arg) {
pthread_mutex_lock(&lock); // Acquire the lock
printf("Thread is executing critical section\n");
pthread_mutex_unlock(&lock); // Release the lock
return NULL;
}
int main() {
pthread_t thread1, thread2;
pthread_mutex_init(&lock, NULL); // Initialize the mutex
// Create two threads
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// Wait for both threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&lock); // Clean up the mutex
return 0;
}
In this example, the mutex ensures that only one thread at a time can enter the critical section where the shared resource is being accessed.
A semaphore is another synchronization primitive, but it is more general than a lock. It is essentially a counter that controls access to resources. Semaphores can be used for both mutual exclusion and synchronization between threads or processes.
A semaphore is initialized with an integer value, and operations on a semaphore involve incrementing or decrementing that value. Semaphores are typically used to signal one or more threads that they can proceed with a certain task or to limit the number of threads that can access a specific resource.
The names P and V come from the Dutch words "proberen" (to test) and "verhogen" (to increment), and are used in some academic contexts.
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
sem_t semaphore; // Declare a semaphore
void* thread_func(void* arg) {
sem_wait(&semaphore); // Decrement semaphore and block if it's 0
printf("Thread is executing critical section\n");
sem_post(&semaphore); // Increment semaphore (release)
return NULL;
}
int main() {
pthread_t thread1, thread2;
sem_init(&semaphore, 0, 1); // Initialize semaphore (binary, value 1)
// Create two threads
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// Wait for both threads to finish
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
sem_destroy(&semaphore); // Clean up the semaphore
return 0;
}
In this example, the semaphore is used to control access to the critical section. If one thread holds the semaphore (decrements the count), the other thread will block until the first thread releases the semaphore by incrementing the count.
While both locks and semaphores are used to manage synchronization, they differ in functionality and use cases.
| Feature | Locks (Mutexes) | Semaphores |
|---|---|---|
| Purpose | Provide mutual exclusion for critical sections of code | Control access to shared resources and synchronize threads |
| Type of Resource | Binary (locked/unlocked) | Can be binary (mutex) or counting (multiple resources) |
| Value | Can only be locked (1) or unlocked (0) | Can hold any integer value (typically ≥ 0) |
| Operations | lock(), unlock() |
wait(), signal() |
| Blocking | A thread blocks until the lock is released | A thread blocks if the semaphore count is 0 (for counting semaphores) |
| Typical Use | Protecting a single resource or critical section | Managing access to a pool of resources (e.g., limiting the number of threads accessing a resource) |
| Counting Ability | No counting mechanism (binary state) | Can count (e.g., counting semaphores) |
Locks and semaphores are essential tools for synchronization in concurrent programming. While locks are best suited for mutual exclusion, where only one thread should access a resource at a time, semaphores provide more general mechanisms for managing resources and synchronizing threads in more complex scenarios. Understanding when and how to use these tools is critical for developing correct, efficient, and scalable parallel programs.
Open this section to load past papers