github.com/haraldrudell/parl@v0.4.176/wait-group-ch.go (about) 1 /* 2 © 2021–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "fmt" 10 "sync" 11 "sync/atomic" 12 13 "github.com/haraldrudell/parl/perrors" 14 ) 15 16 // WaitGroupCh is like a [sync.WaitGroup] with channel-wait mechanic. 17 // therefore, unlike sync, WaitGroupCh is wait-free and observable. 18 // WaitGroupCh waits for a collection of goroutines to finish. 19 // The main goroutine increments the counter for each goroutine. 20 // Then each of the goroutines decrements the counter until zero. 21 // Wait or Ch can be used to block until all goroutines have finished. 22 // A WaitGroup must not be copied after first use. 23 // In the terminology of the Go memory model, decrementing 24 // “synchronizes before” the return of any Wait call or Ch read that it unblocks. 25 // - counter is increased by [WaitGroupCh.Add] or [WaitGroupCh.Count] 26 // - counter is decreased by [WaitGroupCh.Done] [WaitGroupCh.Add] 27 // [WaitGroupCh.DoneBool] or [WaitGroupCh.Count] 28 // - counter zero is awaited by [WaitGroupCh.Ch] or [WaitGroupCh.Wait] 29 // - observability if provided by [WaitGroupCh.Count] [WaitGroupCh.DoneBool] and 30 // [WaitGroupCh.IsZero] 31 // - panic: negative counter from invoking decreasing methods is panic 32 // - panic: adjusting away from zero after invocation of [WaitGroupCh.Ch] or 33 // [WaitGroupCh.Wait] is panic. 34 // NOTE: all Add should happen prior to invoking Ch or Wait 35 // - — 36 // - WaitGroupCh is wait-free, observable, initialization-free, thread-safe 37 // - channel-wait mechanic allows the consumer to be wait-free 38 // Progress by the consumer-thread is not prevented since: 39 // - — the channel can be read non-blocking 40 // - — consumers can wait for multiple channel events 41 // - — consumers are not contending for a lock with any other thread 42 // - WaitGroupCh method-set is a superset of [sync.WaitGroup] 43 // - there are race conditions: 44 // - — writing a zero-counter record with unclosed channel, 45 // therefore closing the channel 46 // - — reading channel event while a zero-counter record exists, 47 // therefore closing the channel 48 // - — impact is that a panic might be missed 49 // 50 // Usage: 51 // 52 // var w parl.WaitGroupCh 53 // w.Add(1) 54 // go someFunc(&w) 55 // … 56 // <-w.Ch() 57 // func someFunc(w parl.Doneable) { 58 // defer w.Done() 59 type WaitGroupCh struct { 60 // p as atomic pointer provides integrity in reading counters without a lock 61 // - atomic Pointer enables initialization-free operation 62 p atomic.Pointer[addsDones] 63 } 64 65 // addsDone provides integrity in reading counters without a lock 66 // - thread-safe access is provided by reading WaitGroupCh.p 67 type addsDones struct { 68 // pointer to be shared across generations 69 ch *Awaitable 70 // cumulative positive adds 71 adds int 72 // cumulative negative adds 73 dones int 74 // flags that further adjustments are not possible 75 channelAboutToClose bool 76 } 77 78 // Done decrements the WaitGroup counter by one. 79 func (w *WaitGroupCh) DoneBool() (isExit bool) { 80 var count, _ = w.Count(-1) 81 return count == 0 82 } 83 84 // Count returns the current state optionally adjusting the counter 85 // - delta is optional counter adjustment 86 // - currentCount is current remaining count 87 // - totalAdds is cumulative positive adds over WaitGroup lifetime 88 func (w *WaitGroupCh) Count(delta ...int) (currentCount, totalAdds int) { 89 var newAddsDones addsDones 90 var channelShouldClose bool 91 for { 92 var p = w.getP() 93 94 // delta absent or zero case: return state 95 if len(delta) == 0 || delta[0] == 0 { 96 currentCount = p.adds - p.dones 97 totalAdds = p.adds 98 return // no adjustment return 99 } 100 101 // check for channel about to close state 102 // - d is known to be non-zero 103 var d = delta[0] 104 if p.channelAboutToClose { 105 panic(perrors.ErrorfPF("attempt to adjust away from zero when Ch or Wait invoked: delta: %d", d)) 106 } 107 108 // create new record 109 newAddsDones = *p 110 if d > 0 { 111 // increment case 112 newAddsDones.adds += d 113 } else if d < 0 { 114 // decrement case 115 if p.dones+(-d) > p.adds { 116 panic(perrors.ErrorfPF("attempt to adjust to negative: d: %d adds: %d dones: %d", 117 d, p.adds, p.dones, 118 )) 119 } 120 newAddsDones.dones += (-d) 121 // check channel should close 122 if newAddsDones.dones == newAddsDones.adds { 123 if channelShouldClose = !newAddsDones.channelAboutToClose; channelShouldClose { 124 newAddsDones.channelAboutToClose = true 125 } 126 } 127 } 128 if w.p.CompareAndSwap(p, &newAddsDones) { 129 break // successfully wrote new record 130 } 131 } 132 currentCount = newAddsDones.adds - newAddsDones.dones 133 totalAdds = newAddsDones.adds 134 if !channelShouldClose { 135 return // not closing channel now 136 } 137 138 // trig the awaitable 139 newAddsDones.ch.Close() 140 141 return 142 } 143 144 // IsZero returns whether the counter is currently zero 145 func (w *WaitGroupCh) IsZero() (isZero bool) { 146 var p = w.getP() 147 return p.adds == p.dones 148 } 149 150 // Add adds delta, which may be negative, to the WaitGroup counter 151 // - If the counter becomes zero, all goroutines blocked on Wait are released 152 // - If the counter goes negative, Add panics 153 func (w *WaitGroupCh) Add(delta int) { w.Count(delta) } 154 155 // Done decrements the WaitGroup counter by one. 156 func (w *WaitGroupCh) Done() { w.Count(-1) } 157 158 // Ch returns a channel that closes once the counter reaches zero 159 func (w *WaitGroupCh) Ch() (awaitableCh AwaitableCh) { return w.getCh() } 160 161 // Wait blocks until the WaitGroup counter is zero. 162 func (w *WaitGroupCh) Wait() { <-w.getCh() } 163 164 // Reset triggers the current channel and resets the WaitGroup 165 func (w *WaitGroupCh) Reset() (w2 *WaitGroupCh) { 166 w2 = w 167 var p = w.p.Swap(nil) 168 if p == nil { 169 return // was not initialized 170 } 171 p.ch.Close() 172 173 return 174 } 175 176 // getCh returns the channel and notes the channel was read 177 func (w *WaitGroupCh) getCh() (awaitableCh AwaitableCh) { 178 var p *addsDones 179 var newAddsDones addsDones 180 var shouldCloseChannel bool 181 for { 182 p = w.getP() 183 if shouldCloseChannel = !p.channelAboutToClose && p.adds == p.dones; !shouldCloseChannel { 184 break // no requirement to take channel close action 185 } 186 newAddsDones = *p 187 p.channelAboutToClose = true 188 if w.p.CompareAndSwap(p, &newAddsDones) { 189 p.ch.Close() 190 break // closed the channel 191 } 192 } 193 194 return p.ch.Ch() 195 } 196 197 // get ensures that WaitGroupCh is initialized 198 func (w *WaitGroupCh) getP() (p *addsDones) { 199 var newAddsDones *addsDones 200 for { 201 if p = w.p.Load(); p != nil { 202 return // already initialized return 203 } else if newAddsDones == nil { 204 newAddsDones = &addsDones{ch: &Awaitable{}} 205 } 206 if w.p.CompareAndSwap(nil, newAddsDones) { 207 p = newAddsDones 208 return // initialized with new p 209 } 210 } 211 } 212 213 func (w *WaitGroupCh) String() (s string) { 214 var p = w.getP() 215 return fmt.Sprintf("waitGroupCh_count:%d(adds:%d)", p.adds-p.dones, p.adds) 216 } 217 218 // func (*sync.WaitGroup).Add(delta int) 219 // func (*sync.WaitGroup).Done() 220 // func (*sync.WaitGroup).Wait() 221 var _ sync.WaitGroup 222 var _ = (&sync.WaitGroup{}).Add 223 var _ = (&sync.WaitGroup{}).Wait 224 var _ = (&sync.WaitGroup{}).Done