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 }