Implementing a Semaphore in Go
Semaphores are fundamental synchronization primitives used in concurrent programming to control access to shared resources. This challenge asks you to implement a semaphore in Go, allowing you to limit the number of goroutines that can simultaneously access a critical section of code. This is crucial for preventing race conditions and ensuring resource availability.
Problem Description
You are tasked with creating a Semaphore type in Go that provides Acquire and Release methods. The Acquire method should block until a permit is available, then decrement the semaphore's counter. The Release method should increment the semaphore's counter, potentially unblocking a waiting goroutine.
Key Requirements:
- Initialization: The
Semaphoreshould be initialized with a given number of permits. - Acquire: Blocks until a permit is available, then decrements the permit count. Should not return if no permit is available.
- Release: Increments the permit count, potentially unblocking a waiting goroutine.
- Thread Safety: The semaphore's internal state (permit count) must be protected against race conditions using appropriate synchronization mechanisms (e.g.,
sync.Mutex). - Blocking Behavior:
Acquiremust block until a permit becomes available. No error should be returned.
Expected Behavior:
- Multiple goroutines can call
Acquireconcurrently. - If the number of goroutines calling
Acquireexceeds the initial number of permits, subsequent calls toAcquirewill block until a permit is released. Releaseshould unblock one waiting goroutine (if any).- The semaphore should maintain a consistent state, even under heavy concurrent access.
Edge Cases to Consider:
- Initialization with zero permits.
- Releasing more permits than initially available (should not cause errors, but the semaphore's state should be capped at its initial value).
- Multiple goroutines waiting on the semaphore.
- Releasing permits after all goroutines have finished acquiring them.
Examples
Example 1:
Input: Semaphore{permits: 2}
Goroutine 1 calls Acquire
Goroutine 2 calls Acquire
Goroutine 3 calls Acquire
Output: Goroutine 1 and Goroutine 2 acquire permits immediately. Goroutine 3 blocks.
Explanation: The semaphore is initialized with 2 permits. The first two goroutines acquire permits without blocking. The third goroutine blocks until a permit is released.
Example 2:
Input: Semaphore{permits: 1}
Goroutine 1 calls Acquire
Goroutine 2 calls Acquire
Goroutine 1 releases permit
Goroutine 3 calls Acquire
Output: Goroutine 1 acquires permit. Goroutine 2 blocks. Goroutine 1 releases permit. Goroutine 2 acquires permit. Goroutine 3 blocks.
Explanation: Goroutine 1 acquires the permit. Goroutine 2 blocks. Goroutine 1 releases the permit, allowing Goroutine 2 to acquire it. Goroutine 3 then blocks.
Example 3: (Edge Case - Zero Permits)
Input: Semaphore{permits: 0}
Goroutine 1 calls Acquire
Goroutine 2 calls Acquire
Goroutine 1 releases permit
Output: Both Goroutine 1 and Goroutine 2 block indefinitely. Goroutine 1 releasing the permit allows Goroutine 2 to acquire it. Goroutine 1 remains blocked.
Explanation: The semaphore is initialized with 0 permits. Both goroutines block indefinitely. Releasing a permit allows one of the blocked goroutines to proceed.
Constraints
- The number of permits during initialization will be a non-negative integer.
- The number of concurrent goroutines accessing the semaphore may be high (up to 1000).
- The semaphore should be implemented efficiently to minimize blocking overhead.
- The code should be well-documented and easy to understand.
Notes
Consider using the sync.Mutex and sync.Cond types from the Go standard library to implement the semaphore. The Cond type is particularly useful for managing waiting goroutines. Think about how to signal waiting goroutines when a permit becomes available. Focus on correctness and thread safety above all else. Avoid busy-waiting; use blocking mechanisms to efficiently wait for permits.