Semaphore
Monitors
Concurrency
Synchronization
Programming Concepts

Semaphore vs. Monitors - what's the difference?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In concurrent programming, handling access to shared resources by multiple threads is critical to ensure data consistency and avoid race conditions. Two common synchronization constructs that facilitate this are semaphores and monitors. Although both serve to manage concurrency, they operate quite differently. In this article, we delve into the specifics of semaphores and monitors, exploring their distinctions, usage scenarios, and implications on program design.

Semaphores

Semaphores are low-level synchronization primitives used to control access to a common resource by multiple processes in a concurrent system. They are simple and versatile, making them suitable for a variety of use cases.

Types of Semaphores

  • Counting Semaphores: These have a value greater than or equal to zero and are used to control access to a given resource with multiple instances. The value of the semaphore represents the number of units of the resource available.
  • Binary Semaphores: Also known as mutexes, these have a value of either 0 or 1, akin to a locking mechanism. They are used for mutual exclusion, ensuring only one thread accesses a critical section at a time.

How Semaphores Work

A semaphore is initialized with a value, and it provides two atomic operations:

  • wait() (also known as P or down operation): Decreases the semaphore's value. If the value is greater than zero, the process continues. If zero or negative, the process is blocked until another process increases the value.
  • signal() (also known as V or up operation): Increases the semaphore's value, potentially waking up a blocked process.

Example

c
1#include <stdio.h>
2#include <pthread.h>
3#include <semaphore.h>
4
5sem_t semaphore;
6
7void* worker(void* id) {
8    sem_wait(&semaphore);  // Decrease semaphore
9    printf("Worker %ld is in the critical section.\n", (long)id);
10    sem_post(&semaphore);  // Increase semaphore
11    return NULL;
12}
13
14int main() {
15    pthread_t threads[5];
16    sem_init(&semaphore, 0, 3);  // Initialize with count 3
17
18    for (long i = 0; i < 5; i++) {
19        pthread_create(&threads[i], NULL, worker, (void*)i);
20    }
21
22    for (int i = 0; i < 5; i++) {
23        pthread_join(threads[i], NULL);
24    }
25
26    sem_destroy(&semaphore);
27    return 0;
28}

In the example above, a counting semaphore allows up to three threads to enter the critical section simultaneously.

Monitors

Monitors are high-level synchronization constructs that provide a mechanism to control access to shared data. They encapsulate variables, the data they operate on, and the operations themselves, combining both mutex and condition variables.

Components of Monitors

  • Mutex: Ensures that only one thread executes within the monitor at a time. This mutex is typically implicit, meaning the programmer does not have to manually manage it.
  • Condition Variables: Permit threads to wait within the monitor if certain conditions are not met, allowing the monitor to optimize control flow and resource allocation.

How Monitors Work

Monitors automatically handle synchronization. By encapsulating shared data, operations, and the synchronization itself, they simplify the complexity of concurrent programming.

Example

java
1class BoundedBuffer {
2    private final int[] buffer;
3    private int count, in, out;
4  
5    public BoundedBuffer(int size) {
6        buffer = new int[size];
7        count = in = out = 0;
8    }
9  
10    public synchronized void insert(int item) throws InterruptedException {
11        while (count == buffer.length)
12            wait(); // Wait until there is space
13        buffer[in] = item;
14        in = (in + 1) % buffer.length;
15        count++;
16        notifyAll(); // Notify waiting threads
17    }
18  
19    public synchronized int remove() throws InterruptedException {
20        while (count == 0)
21            wait(); // Wait until there is an item
22        int item = buffer[out];
23        out = (out + 1) % buffer.length;
24        count--;
25        notifyAll(); // Notify waiting threads
26        return item;
27    }
28}

In the Java example above, the BoundedBuffer class is a typical monitor, managing mutual exclusion and condition synchronization within its methods.

Key Differences Between Semaphores and Monitors

Now that we have explored how semaphores and monitors work, let's summarize the key differences:

FeatureSemaphoresMonitors
Synchronization LevelLow-levelHigh-level
StructureIndependent, not encapsulatedEncapsulates data and operations
PurposeGeneral-purpose synchronizationEncapsulation of access to data
MechanismNeeds explicit wait()/signal()Implicit mutex and condition variables
Complex ScenariosManual handling can lead to complexityNaturally organized within encapsulation
UsageUse when precise control is requiredUse when simplicity and safety are desired

When to Use Each

  • Semaphores are appropriate for controlling access to a resource pool or when lower-level concurrent control is required, giving programmers more control but also more responsibility to ensure correct use.
  • Monitors simplify concurrent programming by abstracting synchronization details, making them ideal for encapsulating data and methods uniformly and optimally.

Conclusion

Understanding and choosing the right synchronization primitive can greatly impact the design and functionality of concurrent programs. Semaphores offer flexibility and control, whereas monitors provide simplicity and safety through abstraction. By grasping these concepts, programmers can better design robust and efficient concurrent applications.


Course illustration
Course illustration