mirror of
https://github.com/redis/go-redis.git
synced 2025-12-02 06:22:31 +03:00
try to fix the semaphore
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user