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.permitsmust 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 inacquire(). - 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 calledacquire()? (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 arelease()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 thanacquire(). It should simply not grant more access than initially available. - The code should be reasonably efficient. Avoid unnecessary locking or copying.
Notes
- Consider using
Mutexto protect the permit count and aCondvarto 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.