Hone logo
Hone
Problems

Implementing a Semaphore in Rust

Semaphores are fundamental synchronization primitives used in concurrent programming to control access to shared resources. This challenge asks you to implement a semaphore in Rust, allowing multiple threads to access a resource, but limiting the number of concurrent accesses. A well-implemented semaphore is crucial for preventing race conditions and ensuring data integrity in multithreaded applications.

Problem Description

You are tasked with implementing a Semaphore struct in Rust that manages a limited number of permits. The semaphore should provide the following functionalities:

  • new(permits: usize): Constructor that initializes the semaphore with a specified number of permits. permits must be greater than zero.
  • acquire(): Acquires a permit. If no permits are available, the calling thread should block until a permit becomes available.
  • release(): Releases a permit, making it available for other threads.
  • available_permits(): Returns the number of currently available permits.

The implementation should be thread-safe, ensuring that multiple threads can safely acquire and release permits concurrently without data races. Use Rust's standard library concurrency primitives (e.g., Mutex, Condvar) to achieve thread safety.

Key Requirements:

  • The acquire() method must block the calling thread if no permits are available.
  • The release() method must wake up a waiting thread if any are blocked in acquire().
  • The available_permits() method should return the correct number of permits.
  • The semaphore should handle edge cases gracefully, such as attempting to release more permits than were initially acquired.

Expected Behavior:

The semaphore should behave as a classic semaphore, controlling access to a resource based on the number of available permits. Threads should block and unblock correctly when acquiring and releasing permits.

Edge Cases to Consider:

  • What happens if a thread calls release() more times than it called acquire()? (Should not panic, but should not grant more access than initially available).
  • What happens if the initial number of permits is zero? (Threads calling acquire() should block indefinitely until a release() is called).
  • Consider the scenario where multiple threads are waiting to acquire a permit. Ensure fairness (though strict fairness isn't required, avoid starvation).

Examples

Example 1:

Input:
Semaphore::new(2);
thread::spawn(|| {
    semaphore.acquire();
    println!("Thread 1 acquired permit");
    thread::sleep(Duration::from_secs(1));
    semaphore.release();
    println!("Thread 1 released permit");
});
thread::spawn(|| {
    semaphore.acquire();
    println!("Thread 2 acquired permit");
    thread::sleep(Duration::from_secs(1));
    semaphore.release();
    println!("Thread 2 released permit");
});
thread::sleep(Duration::from_secs(3));
println!("Available permits: {}", semaphore.available_permits());

Output: (Order may vary due to thread scheduling)

Thread 1 acquired permit
Thread 2 acquired permit
Thread 1 released permit
Thread 2 released permit
Available permits: 2

Explanation: Two threads acquire permits, execute some code, and then release them. The semaphore allows two concurrent accesses.

Example 2:

Input:
Semaphore::new(1);
thread::spawn(|| {
    semaphore.acquire();
    println!("Thread 1 acquired permit");
    thread::sleep(Duration::from_secs(2));
    semaphore.release();
    println!("Thread 1 released permit");
});
thread::spawn(|| {
    thread::sleep(Duration::from_secs(1));
    semaphore.acquire();
    println!("Thread 2 acquired permit");
    semaphore.release();
    println!("Thread 2 released permit");
});
thread::sleep(Duration::from_secs(3));
println!("Available permits: {}", semaphore.available_permits());

Output: (Order may vary due to thread scheduling)

Thread 1 acquired permit
Thread 1 released permit
Thread 2 acquired permit
Thread 2 released permit
Available permits: 1

Explanation: The second thread waits for the first thread to release the permit.

Example 3: (Edge Case)

Input:
Semaphore::new(0);
thread::spawn(|| {
    semaphore.acquire(); // Should block indefinitely
    println!("Thread 1 acquired permit");
});
thread::sleep(Duration::from_secs(2));
semaphore.release();
println!("Available permits: {}", semaphore.available_permits());

Output:

Available permits: 1

Explanation: The thread blocks until release() is called.

Constraints

  • The number of permits in the constructor must be a positive integer (usize > 0).
  • The implementation must be thread-safe.
  • The acquire() method should block indefinitely if no permits are available and no other thread releases a permit.
  • The release() method should not panic if called more times than acquire(). It should simply not grant more access than initially available.
  • The code should be reasonably efficient. Avoid unnecessary locking or copying.

Notes

  • Consider using Mutex to protect the permit count and a Condvar to signal waiting threads when a permit becomes available.
  • Think carefully about the order of acquiring the lock and checking the permit count to avoid race conditions.
  • The available_permits() method should be thread-safe, but doesn't necessarily need to block. It can return a snapshot of the permit count.
  • This is a good opportunity to practice using Rust's concurrency primitives and understanding the principles of synchronization.
Loading editor...
rust