Implementing a Semaphore in Go
Concurrency is a fundamental aspect of modern software development. Semaphores are a powerful synchronization primitive that help manage access to shared resources, preventing race conditions and ensuring orderly execution of concurrent operations. This challenge requires you to implement a basic semaphore in Go, a common tool for controlling the number of goroutines that can access a particular section of code or resource simultaneously.
Problem Description
Your task is to implement a semaphore data structure in Go. A semaphore controls access to a shared resource by maintaining a counter. When a goroutine needs to access the resource, it must acquire a "permit" from the semaphore. If no permits are available, the goroutine will block until one is released. When the goroutine finishes with the resource, it must release the permit, making it available for other waiting goroutines.
Key Requirements:
- Initialization: The semaphore must be initialized with a specified number of available permits.
- Acquire (Wait): A method to acquire a permit. If no permits are available, the goroutine calling this method should block until a permit becomes available.
- Release (Signal): A method to release a permit, making it available for other goroutines.
- Thread-Safety: All operations (initialization, acquire, release) must be thread-safe, meaning they can be called concurrently from multiple goroutines without causing data corruption or unexpected behavior.
Expected Behavior:
- A semaphore initialized with
Npermits should allow up toNgoroutines to acquire a permit concurrently. - The
Acquireoperation should block if allNpermits are currently held. - The
Releaseoperation should unblock one waiting goroutine if there are any waiting.
Edge Cases:
- Initializing a semaphore with zero permits.
- Calling
Releasemore times thanAcquirehas been called (this should not cause issues, but the counter should not go below zero). - A large number of goroutines attempting to acquire permits simultaneously.
Examples
Example 1:
// Pseudocode illustrating behavior
semaphore := NewSemaphore(2) // Initialize with 2 permits
go func() {
semaphore.Acquire()
// Access shared resource
fmt.Println("Goroutine 1 acquired permit")
time.Sleep(1 * time.Second)
semaphore.Release()
fmt.Println("Goroutine 1 released permit")
}()
go func() {
semaphore.Acquire()
// Access shared resource
fmt.Println("Goroutine 2 acquired permit")
time.Sleep(1 * time.Second)
semaphore.Release()
fmt.Println("Goroutine 2 released permit")
}()
go func() {
semaphore.Acquire()
// Access shared resource
fmt.Println("Goroutine 3 acquired permit")
time.Sleep(1 * time.Second)
semaphore.Release()
fmt.Println("Goroutine 3 released permit")
}()
// Expected Output (order of 'acquired' might vary, but only two will print before 'released'):
// Goroutine 1 acquired permit
// Goroutine 2 acquired permit
// Goroutine 1 released permit
// Goroutine 3 acquired permit
// Goroutine 2 released permit
// Goroutine 3 released permit
Explanation: Two goroutines can acquire permits immediately. The third goroutine will block until one of the first two releases their permit.
Example 2:
// Pseudocode illustrating behavior
semaphore := NewSemaphore(1) // Initialize with 1 permit
go func() {
semaphore.Acquire()
fmt.Println("Goroutine A acquired permit")
time.Sleep(500 * time.Millisecond)
semaphore.Release()
fmt.Println("Goroutine A released permit")
}()
go func() {
semaphore.Acquire()
fmt.Println("Goroutine B acquired permit")
time.Sleep(500 * time.Millisecond)
semaphore.Release()
fmt.Println("Goroutine B released permit")
}()
// Expected Output:
// Goroutine A acquired permit
// Goroutine A released permit
// Goroutine B acquired permit
// Goroutine B released permit
Explanation: With only one permit, goroutines acquire and release the permit sequentially. Goroutine B must wait for Goroutine A to release its permit.
Constraints
- The initial number of permits (
N) will be a non-negative integer. - Your implementation should be able to handle a large number of concurrent goroutines (e.g., thousands).
- The
AcquireandReleaseoperations should ideally have a time complexity that is proportional to the number of waiting goroutines in the worst case, but generally very efficient.
Notes
- You will need to use Go's built-in concurrency primitives, such as
sync.Mutexandsync.Cond(orchanfor simpler scenarios if appropriate), to ensure thread-safety and blocking behavior. - Consider how to manage the state of the semaphore (e.g., the current count of available permits and a queue of waiting goroutines).
- The goal is to implement the semaphore logic yourself, not to simply use
sync.WaitGroupor other higher-level abstractions that might mask the underlying semaphore behavior. You can, however, usesync.Mutexandsync.Condas building blocks.