Hone logo
Hone
Problems

Python Semaphore Implementation Challenge

This challenge involves creating a basic semaphore mechanism in Python. Semaphores are synchronization primitives used to control access to a shared resource by multiple threads. Implementing one helps understand fundamental concurrency control concepts.

Problem Description

Your task is to create a Python class named Semaphore that mimics the behavior of a standard semaphore. This semaphore should manage a counter representing the number of available permits. Threads will attempt to acquire a permit (decrementing the counter) and release a permit (incrementing the counter). If no permits are available when a thread tries to acquire one, that thread should block until a permit is released.

Key Requirements:

  • The Semaphore class should accept an initial value (number of permits) upon instantiation.
  • It must provide two primary methods:
    • acquire(self): Attempts to acquire a permit. If value is greater than 0, it decrements value and returns True. If value is 0, it blocks the calling thread until value becomes greater than 0, then decrements and returns True.
    • release(self): Increments the value. This operation should unblock any threads waiting on acquire.
  • The implementation must be thread-safe to prevent race conditions.

Expected Behavior:

  • Multiple threads can call acquire concurrently.
  • Only a limited number of threads (equal to the initial value) can successfully acquire permits simultaneously.
  • Threads attempting to acquire when no permits are available should wait gracefully.
  • release should correctly signal waiting threads.

Edge Cases:

  • Initializing the semaphore with a value of 0.
  • Releasing a semaphore that has no threads waiting on it.
  • Multiple threads calling release rapidly.

Examples

Example 1:

import threading
import time

def worker(semaphore, worker_id):
    print(f"Worker {worker_id}: Trying to acquire semaphore.")
    semaphore.acquire()
    print(f"Worker {worker_id}: Acquired semaphore. Working...")
    time.sleep(1) # Simulate work
    print(f"Worker {worker_id}: Releasing semaphore.")
    semaphore.release()

# Initializing semaphore with 2 permits
semaphore = Semaphore(2)
threads = []

for i in range(5):
    thread = threading.Thread(target=worker, args=(semaphore, i))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print("All workers finished.")

Output (order of "Acquired semaphore" and "Releasing semaphore" might vary slightly due to thread scheduling):

Worker 0: Trying to acquire semaphore.
Worker 0: Acquired semaphore. Working...
Worker 1: Trying to acquire semaphore.
Worker 1: Acquired semaphore. Working...
Worker 2: Trying to acquire semaphore.
Worker 3: Trying to acquire semaphore.
Worker 4: Trying to acquire semaphore.
Worker 0: Releasing semaphore.
Worker 2: Acquired semaphore. Working...
Worker 1: Releasing semaphore.
Worker 3: Acquired semaphore. Working...
Worker 2: Releasing semaphore.
Worker 4: Acquired semaphore. Working...
Worker 3: Releasing semaphore.
Worker 4: Releasing semaphore.
All workers finished.

Explanation: Five worker threads try to acquire a semaphore initialized with 2 permits. Only two workers can acquire the semaphore at a time. As workers finish and release their permits, waiting workers are allowed to proceed.

Example 2:

import threading
import time

def producer(semaphore, item_count):
    print("Producer: Starting.")
    for i in range(item_count):
        print(f"Producer: Producing item {i}. Waiting to acquire semaphore.")
        semaphore.acquire()
        print(f"Producer: Acquired semaphore. Added item {i} to buffer.")
        time.sleep(0.5) # Simulate adding to buffer
        print(f"Producer: Releasing semaphore after adding item {i}.")
        semaphore.release()
    print("Producer: Finished producing.")

# Initializing semaphore with 1 permit (simulating a single-slot buffer)
semaphore = Semaphore(1)
producer_thread = threading.Thread(target=producer, args=(semaphore, 3))
producer_thread.start()
producer_thread.join()

print("Producer challenge finished.")

Output:

Producer: Starting.
Producer: Producing item 0. Waiting to acquire semaphore.
Producer: Acquired semaphore. Added item 0 to buffer.
Producer: Releasing semaphore after adding item 0.
Producer: Producing item 1. Waiting to acquire semaphore.
Producer: Acquired semaphore. Added item 1 to buffer.
Producer: Releasing semaphore after adding item 1.
Producer: Producing item 2. Waiting to acquire semaphore.
Producer: Acquired semaphore. Added item 2 to buffer.
Producer: Releasing semaphore after adding item 2.
Producer: Finished producing.
Producer challenge finished.

Explanation: The producer thread adds items to a conceptual buffer with a capacity of 1, enforced by the semaphore. It must acquire a permit before adding an item and release it afterward, ensuring only one item is processed at a time.

Constraints

  • The Semaphore class must be implemented using Python's standard threading module primitives (e.g., Lock and Condition). Do not use threading.Semaphore or any other pre-built semaphore classes.
  • The initial value of the semaphore must be a non-negative integer.
  • The solution should be efficient and avoid busy-waiting.

Notes

  • You will need to manage the internal value (number of permits) and use synchronization primitives to ensure thread safety and blocking behavior.
  • Consider how to signal waiting threads when a permit is released. A threading.Condition object is a good candidate for this.
  • Think about the order of operations within acquire and release to correctly handle the counter and waiting threads.
  • This challenge is about understanding the internal workings of synchronization primitives.
Loading editor...
python