github.com/GoWebProd/gip@v0.0.0-20230623090727-b60d41d5d320/pool/dequeue.go (about)

     1  package pool
     2  
     3  import (
     4  	"sync/atomic"
     5  )
     6  
     7  const (
     8  	dequeueBits = 32
     9  
    10  	// dequeueLimit is the maximum size of a poolDequeue.
    11  	//
    12  	// This must be at most (1<<dequeueBits)/2 because detecting fullness
    13  	// depends on wrapping around the ring buffer without wrapping around
    14  	// the index. We divide by 4 so this fits in an int on 32-bit.
    15  	dequeueLimit = (1 << dequeueBits) / 4
    16  )
    17  
    18  // poolDequeue is a lock-free fixed-size single-producer,
    19  // multi-consumer queue. The single producer can both push and pop
    20  // from the head, and consumers can pop from the tail.
    21  //
    22  // It has the added feature that it nils out unused slots to avoid
    23  // unnecessary retention of objects. This is important for sync.Pool,
    24  // but not typically a property considered in the literature.
    25  type poolDequeue[T any] struct {
    26  	// headTail packs together a 32-bit head index and a 32-bit
    27  	// tail index. Both are indexes into vals modulo len(vals)-1.
    28  	//
    29  	// tail = index of oldest data in queue
    30  	// head = index of next slot to fill
    31  	//
    32  	// Slots in the range [tail, head) are owned by consumers.
    33  	// A consumer continues to own a slot outside this range until
    34  	// it nils the slot, at which point ownership passes to the
    35  	// producer.
    36  	//
    37  	// The head index is stored in the most-significant bits so
    38  	// that we can atomically add to it and the overflow is
    39  	// harmless.
    40  	headTail uint64
    41  
    42  	// vals is a ring buffer of interface{} values stored in this
    43  	// dequeue. The size of this must be a power of 2.
    44  	//
    45  	// vals[i].typ is nil if the slot is empty and non-nil
    46  	// otherwise. A slot is still in use until *both* the tail
    47  	// index has moved beyond it and typ has been set to nil. This
    48  	// is set to nil atomically by the consumer and read
    49  	// atomically by the producer.
    50  	vals []*T
    51  }
    52  
    53  func (d *poolDequeue[T]) pack(head, tail uint32) uint64 {
    54  	const mask = 1<<dequeueBits - 1
    55  	return (uint64(head) << dequeueBits) | uint64(tail&mask)
    56  }
    57  
    58  func (d *poolDequeue[T]) unpack(ptrs uint64) (head, tail uint32) {
    59  	const mask = 1<<dequeueBits - 1
    60  	head = uint32((ptrs >> dequeueBits) & mask)
    61  	tail = uint32(ptrs & mask)
    62  	return
    63  }
    64  
    65  // pushHead adds val at the head of the queue. It returns false if the
    66  // queue is full. It must only be called by a single producer.
    67  func (d *poolDequeue[T]) pushHead(val *T) bool {
    68  	ptrs := atomic.LoadUint64(&d.headTail)
    69  	head, tail := d.unpack(ptrs)
    70  	if (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head {
    71  		// Queue is full.
    72  		return false
    73  	}
    74  	slot := &d.vals[head&uint32(len(d.vals)-1)]
    75  
    76  	// Check if the head slot has been released by popTail.
    77  	if *slot != nil {
    78  		// Another goroutine is still cleaning up the tail, so
    79  		// the queue is actually still full.
    80  		return false
    81  	}
    82  
    83  	*slot = val
    84  
    85  	// Increment head. This passes ownership of slot to popTail
    86  	// and acts as a store barrier for writing the slot.
    87  	atomic.AddUint64(&d.headTail, 1<<dequeueBits)
    88  	return true
    89  }
    90  
    91  // popHead removes and returns the element at the head of the queue.
    92  // It returns false if the queue is empty. It must only be called by a
    93  // single producer.
    94  func (d *poolDequeue[T]) popHead() (*T, bool) {
    95  	var slot **T
    96  	for {
    97  		ptrs := atomic.LoadUint64(&d.headTail)
    98  		head, tail := d.unpack(ptrs)
    99  
   100  		if tail == head {
   101  			// Queue is empty.
   102  			return nil, false
   103  		}
   104  
   105  		// Confirm tail and decrement head. We do this before
   106  		// reading the value to take back ownership of this
   107  		// slot.
   108  		head--
   109  
   110  		ptrs2 := d.pack(head, tail)
   111  
   112  		if atomic.CompareAndSwapUint64(&d.headTail, ptrs, ptrs2) {
   113  			// We successfully took back slot.
   114  			slot = &d.vals[head&uint32(len(d.vals)-1)]
   115  
   116  			break
   117  		}
   118  	}
   119  
   120  	val := *slot
   121  	// Zero the slot. Unlike popTail, this isn't racing with
   122  	// pushHead, so we don't need to be careful here.
   123  	*slot = nil
   124  
   125  	return val, true
   126  }
   127  
   128  // popTail removes and returns the element at the tail of the queue.
   129  // It returns false if the queue is empty. It may be called by any
   130  // number of consumers.
   131  func (d *poolDequeue[T]) popTail() (*T, bool) {
   132  	var slot **T
   133  	for {
   134  		ptrs := atomic.LoadUint64(&d.headTail)
   135  		head, tail := d.unpack(ptrs)
   136  		if tail == head {
   137  			// Queue is empty.
   138  			return nil, false
   139  		}
   140  
   141  		// Confirm head and tail (for our speculative check
   142  		// above) and increment tail. If this succeeds, then
   143  		// we own the slot at tail.
   144  		ptrs2 := d.pack(head, tail+1)
   145  		if atomic.CompareAndSwapUint64(&d.headTail, ptrs, ptrs2) {
   146  			// Success.
   147  			slot = &d.vals[tail&uint32(len(d.vals)-1)]
   148  			break
   149  		}
   150  	}
   151  
   152  	// We now own slot.
   153  	val := *slot
   154  
   155  	// Tell pushHead that we're done with this slot. Zeroing the
   156  	// slot is also important so we don't leave behind references
   157  	// that could keep this object live longer than necessary.
   158  	//
   159  	// We write to val first and then publish that we're done with
   160  	// this slot by atomically writing to typ.
   161  	slot = nil
   162  
   163  	return val, true
   164  }