1
0
mirror of https://github.com/redis/go-redis.git synced 2025-12-03 18:31:14 +03:00

try to fix the semaphore

This commit is contained in:
Nedyalko Dyakov
2025-11-10 13:29:23 +02:00
parent 1dfa805f31
commit e418737532

View File

@@ -15,17 +15,28 @@ var semTimers = sync.Pool{
}, },
} }
// waiter represents a goroutine waiting for a token.
type waiter struct {
ready chan struct{}
next *waiter
cancelled atomic.Bool // Set to true if this waiter was cancelled/timed out
}
// FastSemaphore is a counting semaphore implementation using atomic operations. // FastSemaphore is a counting semaphore implementation using atomic operations.
// It's optimized for the fast path (no blocking) while still supporting timeouts and context cancellation. // It's optimized for the fast path (no blocking) while still supporting timeouts and context cancellation.
// //
// This implementation maintains FIFO ordering of waiters using a linked list queue.
// When a token is released, the first waiter in the queue is notified.
//
// Performance characteristics: // Performance characteristics:
// - Fast path (no blocking): Single atomic CAS operation // - Fast path (no blocking): Single atomic CAS operation
// - Slow path (blocking): Falls back to channel-based waiting // - Slow path (blocking): FIFO queue-based waiting
// - Release: Single atomic decrement + optional channel notification // - Release: Single atomic decrement + wake up first waiter in queue
// //
// This is significantly faster than a pure channel-based semaphore because: // This is significantly faster than a pure channel-based semaphore because:
// 1. The fast path avoids channel operations entirely (no scheduler involvement) // 1. The fast path avoids channel operations entirely (no scheduler involvement)
// 2. Atomic operations are much cheaper than channel send/receive // 2. Atomic operations are much cheaper than channel send/receive
// 3. FIFO ordering prevents starvation
type FastSemaphore struct { type FastSemaphore struct {
// Current number of acquired tokens (atomic) // Current number of acquired tokens (atomic)
count atomic.Int32 count atomic.Int32
@@ -33,16 +44,18 @@ type FastSemaphore struct {
// Maximum number of tokens (capacity) // Maximum number of tokens (capacity)
max int32 max int32
// Channel for blocking waiters // Mutex to protect the waiter queue
// Only used when fast path fails (semaphore is full) lock sync.Mutex
waitCh chan struct{}
// Head and tail of the waiter queue (FIFO)
head *waiter
tail *waiter
} }
// NewFastSemaphore creates a new fast semaphore with the given capacity. // NewFastSemaphore creates a new fast semaphore with the given capacity.
func NewFastSemaphore(capacity int32) *FastSemaphore { func NewFastSemaphore(capacity int32) *FastSemaphore {
return &FastSemaphore{ return &FastSemaphore{
max: capacity, max: capacity,
waitCh: make(chan struct{}, capacity),
} }
} }
@@ -63,51 +76,153 @@ func (s *FastSemaphore) TryAcquire() bool {
} }
} }
// enqueue adds a waiter to the end of the queue.
// Must be called with lock held.
func (s *FastSemaphore) enqueue(w *waiter) {
if s.tail == nil {
s.head = w
s.tail = w
} else {
s.tail.next = w
s.tail = w
}
}
// dequeue removes and returns the first waiter from the queue.
// Must be called with lock held.
// Returns nil if the queue is empty.
func (s *FastSemaphore) dequeue() *waiter {
if s.head == nil {
return nil
}
w := s.head
s.head = w.next
if s.head == nil {
s.tail = nil
}
w.next = nil
return w
}
// notifyOne wakes up the first waiter in the queue if any.
func (s *FastSemaphore) notifyOne() {
s.lock.Lock()
w := s.dequeue()
s.lock.Unlock()
if w != nil {
close(w.ready)
}
}
// Acquire acquires a token, blocking if necessary until one is available or the context is cancelled. // Acquire acquires a token, blocking if necessary until one is available or the context is cancelled.
// Returns an error if the context is cancelled or the timeout expires. // Returns an error if the context is cancelled or the timeout expires.
// Returns timeoutErr when the timeout expires. // Returns timeoutErr when the timeout expires.
//
// Performance optimization:
// 1. First try fast path (no blocking)
// 2. If that fails, fall back to channel-based waiting
func (s *FastSemaphore) Acquire(ctx context.Context, timeout time.Duration, timeoutErr error) error { func (s *FastSemaphore) Acquire(ctx context.Context, timeout time.Duration, timeoutErr error) error {
// Fast path: try to acquire without blocking // Check context first
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: default:
} }
// Try fast acquire first // Try fast path first
if s.TryAcquire() { if s.TryAcquire() {
return nil return nil
} }
// Fast path failed, need to wait // Need to wait - create a waiter and add to queue
w := &waiter{
ready: make(chan struct{}),
}
s.lock.Lock()
s.enqueue(w)
s.lock.Unlock()
// Use timer pool to avoid allocation // Use timer pool to avoid allocation
timer := semTimers.Get().(*time.Timer) timer := semTimers.Get().(*time.Timer)
defer semTimers.Put(timer) defer semTimers.Put(timer)
timer.Reset(timeout) timer.Reset(timeout)
for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
if !timer.Stop() { if !timer.Stop() {
<-timer.C <-timer.C
} }
return ctx.Err() // Mark ourselves as cancelled
w.cancelled.Store(true)
// Try to remove ourselves from the queue
s.lock.Lock()
removed := s.removeWaiter(w)
s.lock.Unlock()
case <-s.waitCh: if !removed {
// Someone released a token, we got the spot // We were already dequeued and notified
// no need to touch the counter // Wait for the notification and then release the token
<-w.ready
s.releaseToPool()
}
return ctx.Err()
case <-w.ready:
// We were notified, check if we were cancelled
if !timer.Stop() { if !timer.Stop() {
<-timer.C <-timer.C
} }
if w.cancelled.Load() {
// We were cancelled while being notified, release the token
s.releaseToPool()
return ctx.Err()
}
return nil return nil
case <-timer.C: case <-timer.C:
// Mark ourselves as cancelled
w.cancelled.Store(true)
// Try to remove ourselves from the queue
s.lock.Lock()
removed := s.removeWaiter(w)
s.lock.Unlock()
if !removed {
// We were already dequeued and notified
// Wait for the notification and then release the token
<-w.ready
s.releaseToPool()
}
return timeoutErr return timeoutErr
} }
}
// removeWaiter removes a waiter from the queue.
// Must be called with lock held.
// Returns true if the waiter was found and removed, false otherwise.
func (s *FastSemaphore) removeWaiter(target *waiter) bool {
if s.head == nil {
return false
} }
// Special case: removing head
if s.head == target {
s.head = target.next
if s.head == nil {
s.tail = nil
}
return true
}
// Find and remove from middle or tail
prev := s.head
for prev.next != nil {
if prev.next == target {
prev.next = target.next
if prev.next == nil {
s.tail = prev
}
return true
}
prev = prev.next
}
return false
} }
// AcquireBlocking acquires a token, blocking indefinitely until one is available. // AcquireBlocking acquires a token, blocking indefinitely until one is available.
@@ -119,29 +234,62 @@ func (s *FastSemaphore) AcquireBlocking() {
return return
} }
// Slow path: wait for a token // Need to wait - create a waiter and add to queue
for { w := &waiter{
<-s.waitCh ready: make(chan struct{}),
// Someone released a token, we got the spot }
// no need to touch the counter
return s.lock.Lock()
s.enqueue(w)
s.lock.Unlock()
// Wait to be notified
<-w.ready
}
// releaseToPool releases a token back to the pool.
// This should be called when a waiter was notified but then cancelled/timed out.
// We need to pass the token to another waiter if any, otherwise decrement the counter.
func (s *FastSemaphore) releaseToPool() {
s.lock.Lock()
w := s.dequeue()
s.lock.Unlock()
if w != nil {
// Transfer the token to another waiter
close(w.ready)
} else {
// No waiters, decrement the counter to free the slot
s.count.Add(-1)
} }
} }
// Release releases a token back to the semaphore. // Release releases a token back to the semaphore.
// This wakes up one waiting goroutine if any are blocked. // This wakes up the first waiting goroutine if any are blocked.
func (s *FastSemaphore) Release() { func (s *FastSemaphore) Release() {
for {
s.lock.Lock()
w := s.dequeue()
s.lock.Unlock()
// Try to wake up a waiter (non-blocking) if w == nil {
// If no one is waiting, this is a no-op // No waiters, decrement the counter to free the slot
select {
case s.waitCh <- struct{}{}:
// Successfully notified a waiter
// no need to decrement the counter, the waiter will use this spot
default:
// No waiters, that's fine
// decrement the counter
s.count.Add(-1) s.count.Add(-1)
return
}
// Check if this waiter was cancelled before we notify them
if w.cancelled.Load() {
// This waiter was cancelled, skip them and try the next one
// We still have the token, so continue the loop
close(w.ready) // Still need to close to unblock them
continue
}
// Transfer the token directly to the waiter
// Don't decrement the counter - the waiter takes over this slot
close(w.ready)
return
} }
} }