github.com/haraldrudell/parl@v0.4.176/counting-awaitable.go (about) 1 /* 2 © 2023-present Harald Rudell <haraldrudell@proton.me> (https://haraldrudell.github.io/haraldrudell/) 3 All rights reserved 4 */ 5 6 package parl 7 8 import ( 9 "sync/atomic" 10 11 "github.com/haraldrudell/parl/perrors" 12 ) 13 14 const ( 15 bTrigBit = 0x1 16 bNegativeBit = 0x2 17 bShift = 2 18 ) 19 20 type CountingAwaitable struct { 21 Awaitable 22 counter atomic.Int64 23 adds atomic.Uint64 24 } 25 26 // NewCountingAwaitable returns a counting awaitable 27 // - similar to sync.WaitGroup but wait-mechanic is closing channel 28 // - observable via Count method. 29 // Count with missing delta will not panic 30 // - — 31 // - state is determined by counter. 32 // Guaranteed state is returned by Count IsTriggered 33 // - IsClosed is eventually consistent. 34 // A parallel Count may reflect triggered prior IsClose returning true. 35 // Upon return from the effective Count Add Done IsTriggered invocation, 36 // IsClose is consistent and the channel is closed if it is to close. 37 // For race-sensitive code, synchronize or rely on Count 38 // - similarly, a parallel Count invocation may reflect triggered state 39 // prior to the Awaitable channel actually closing 40 // - mechanic is atomic.Int64.CompareAndSwap 41 func NewCountingAwaitable() (awaitable *CountingAwaitable) { 42 return &CountingAwaitable{} 43 } 44 45 // IsTriggered returns true if counter has been used and returned to zero 46 // - IsClosed true, AwaitableCh closed 47 func (a *CountingAwaitable) IsTriggered() (isTriggered bool) { 48 return a.counter.Load()&bTrigBit != 0 49 } 50 51 // Count returns the current count and may also adjust the counter 52 // - counter is current remaining count 53 // - adds is cumulative positive adds 54 // - if delta is present and the awaitable is triggered, this is panic 55 // - state can be retrieved without panic by omitting delta 56 // - if delta is negative and result is zero: trig 57 // - if delta is negative and result is negative: panic 58 // - similar to sync.WaitGroup.Add Done 59 func (a *CountingAwaitable) Count(delta ...int) (counter, adds int) { 60 var hasDelta = len(delta) > 0 61 if !hasDelta { 62 counter = int(a.counter.Load() >> bShift) 63 adds = int(a.adds.Load()) 64 return 65 } 66 var delta0 = delta[0] 67 68 // thread-safe add to counter 69 var counter64 int64 70 for { 71 var value = a.counter.Load() 72 if value&bNegativeBit != 0 { 73 panic(perrors.NewPF("negative fail-state encountered")) 74 } else if value&bTrigBit != 0 { 75 panic(perrors.NewPF("Add or Done on triggered CountingAwaitable")) 76 } 77 if delta0 == 0 { 78 counter = int(value >> bShift) 79 adds = int(a.adds.Load()) 80 return // noop 81 } 82 counter64 = value + int64(delta0)<<bShift 83 if counter64 == 0 { 84 counter64 |= bTrigBit 85 } else if counter < 0 { 86 counter |= bNegativeBit 87 } 88 if a.counter.CompareAndSwap(value, counter64) { 89 break // add succeeded 90 } 91 } 92 counter = int(counter64 >> bShift) 93 if delta0 > 0 { 94 adds = int(a.adds.Add(uint64(delta0))) 95 } else { 96 adds = int(a.adds.Load()) 97 } 98 if counter64 != bTrigBit && counter > 0 { 99 return // no trig or negative 100 } else if counter64 < 0 { 101 panic(perrors.NewPF("negative counter")) 102 } 103 104 // trig! 105 if !a.Close() { 106 panic(perrors.NewPF("Awaitable already triggered")) 107 } 108 109 return 110 } 111 112 // Add signals thread-launch by adding to the counter 113 // - if delta is negative and result is zero: trig 114 // - if delta is negative and result is negative: panic 115 // - Add on triggered or negative is panic 116 // - similar to sync.WaitGroup.Add 117 func (a *CountingAwaitable) Add(delta int) { a.Count(delta) } 118 119 // Done signals thread exit by decrementing the counter 120 // - if delta is negative and result is zero: trig 121 // - if delta is negative and result is negative: panic 122 // - Add on triggered or negative is panic 123 // - similar to sync.WaitGroup.Done 124 func (a *CountingAwaitable) Done() { a.Count(-1) }