Mastering Concurrency: Using Semaphores for Effective Resource Managementblog

Brandon Kindred
4 min readJust now

--

Photo by Niko Lewman on Unsplash

Semaphores offer a more structured and flexible way to manage resource access and prevent issues like deadlock and starvation. While locks offer basic resource control, semaphores provide additional functionality that can make the solution more robust and efficient. If you aren’t familar with locks check out my other blog post on the Dining Philosopher problem.

Let’s explore what semaphores are and how they can be applied to the Dining Philosopher Problem.

What is a Semaphore?

A semaphore is a synchronization primitive used in concurrent programming to control access to a common resource by multiple processes. It is essentially a counter that tracks the number of resources available. When a process wants to use a resource, it “acquires” (decrements) the semaphore. When it’s done, it “releases” (increments) the semaphore.

There are two main types of semaphores:

  1. Binary Semaphore (also called a mutex): It acts like a lock, allowing only one process to access a resource at a time. The counter for a binary semaphore can only be 0 or 1.
  2. Counting Semaphore: This allows access to a resource by multiple processes but limits the number of processes that can hold the resource simultaneously. The counter can be greater than 1, representing the number of available resources.

How Semaphores Help in the Dining Philosopher Problem

In the Dining Philosopher Problem, semaphores can help by controlling how many philosophers can access the forks (shared resources) at the same time. Semaphores provide more flexibility than locks because they can impose additional constraints to avoid deadlock and starvation.

1. Preventing Deadlock with a Counting Semaphore

One effective way to apply semaphores to the Dining Philosopher Problem is to use a counting semaphore to limit the number of philosophers who can try to pick up forks at the same time. By limiting the number of philosophers who can simultaneously sit at the table, you reduce the chances of a deadlock.

For example:

  • If there are five philosophers, we could use a semaphore that allows up to four philosophers to pick up forks. This ensures that at least one philosopher will always be able to pick up both forks (since there are more forks than there are philosophers actively trying to eat).

2. Binary Semaphore for Fork Access

Each fork can be represented by a binary semaphore (like a lock). A philosopher can only pick up a fork if the semaphore allows it. If the semaphore for a fork is unavailable (meaning another philosopher is holding the fork), the philosopher must wait until it becomes available again.

Python Implementation Using Semaphores

Let’s modify the earlier implementation of the Dining Philosopher Problem to use semaphores.

import threading
import time

class Philosopher(threading.Thread):
def __init__(self, name, left_fork, right_fork, max_philosophers):
threading.Thread.__init__(self)
self.name = name
self.left_fork = left_fork
self.right_fork = right_fork
self.max_philosophers = max_philosophers

def run(self):
while True:
print(f"{self.name} is thinking.")
time.sleep(1)
print(f"{self.name} is hungry.")
self.dine()

def dine(self):
# Limit the number of philosophers who can attempt to pick up forks
self.max_philosophers.acquire()
print(f"{self.name} is trying to pick up forks.")
self.left_fork.acquire()
self.right_fork.acquire()
print(f"{self.name} is eating.")
time.sleep(2) # Simulate eating
print(f"{self.name} finished eating.")
self.left_fork.release()
self.right_fork.release()
# Allow other philosophers to attempt to pick up forks
self.max_philosophers.release()


# Create semaphores for forks (binary semaphores)
forks = [threading.Semaphore(1) for _ in range(5)]

# Create a counting semaphore to limit the number of philosophers trying to eat
max_philosophers = threading.Semaphore(4) # Allow up to 4 philosophers at a time

# Create philosophers
philosophers = [
Philosopher(f"Philosopher {i + 1}", forks[i % 5], forks[(i + 1) % 5], max_philosophers)
for i in range(5)
]

# Start the philosopher threads
for philosopher in philosophers:
philosopher.start()

Explanation of the Semaphore Solution

1. Counting Semaphore for Philosophers

We use a counting semaphore max_philosophers = threading.Semaphore(4) to control how many philosophers can attempt to pick up forks at the same time. This is key to preventing deadlock because it ensures that at least one philosopher will always be able to access both forks without waiting on others.

In this example, only 4 philosophers can attempt to pick up forks at a time. The fifth philosopher will have to wait until one of the others is done eating.

2. Binary Semaphores for Forks

Each fork is represented by a binary semaphore (threading.Semaphore(1)), ensuring that only one philosopher can use each fork at any given time. A philosopher "acquires" a fork by calling acquire() on the semaphore, and when they are done eating, they "release" the fork using release().

3. Preventing Deadlock

By using a counting semaphore to limit the number of philosophers trying to eat, we prevent the situation where all philosophers are holding one fork and waiting indefinitely for the second one. The counting semaphore ensures that fewer philosophers try to eat than there are available forks, allowing at least one philosopher to get both forks and finish eating.

4. Fairness

By using semaphores, we ensure fairness in resource access. Philosophers will eventually get their turn to eat without the risk of indefinite waiting (starvation).

5. Flexibility

Semaphores provide more flexibility than simple locks. For example, if you wanted to adjust the number of philosophers or forks dynamically, semaphores can be easily adjusted to handle different numbers of philosophers or resources.

Wrapping Up

Semaphores offer a more flexible and robust way to solve the Dining Philosopher Problem compared to using just locks. By controlling how many philosophers can try to eat at once and ensuring that forks (resources) are acquired and released safely, semaphores can help prevent issues like deadlock and starvation while maintaining fairness and concurrency.

In real-world systems, semaphores are widely used to manage resource access in database systems, operating systems, and distributed systems where multiple processes or threads need to share resources efficiently.

This approach is not only applicable to thought experiments like the Dining Philosopher Problem but also to complex, real-world systems where managing shared resources is key to maintaining performance and reliability.

Happy coding!

--

--

Brandon Kindred

Entrepreneur & computer science nerd that is passionate about AI, robotics, & startup life. Currently focused on improving UX processes at https://Look-see.com