github.com/haraldrudell/parl@v0.4.176/nb-chan-close.go (about) 1 /* 2 © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "github.com/haraldrudell/parl/perrors" 10 ) 11 12 // Close closes the underlying channel without data loss 13 // - didClose true: this Close invocation executed channel close 14 // - didClose may be false for all invocations if the channel is closed by sendThread 15 // - when Close returns, the channel may still be open and have items 16 // - Close is thread-safe, non-blocking, error-free and panic-free 17 // - underlying channel closes once Send SendMany completes and the channel 18 // is empty 19 func (n *NBChan[T]) Close() (didClose bool) { 20 21 // atomic performance check if Close already invoked 22 if n.isCloseInvoked.IsInvoked() { 23 // await winner return 24 <-n.isCloseInvoked.Ch() 25 return // Close complete return 26 } 27 28 // select Close winner 29 var isWinner, isRunningThread, done = n.selectCloseWinner() 30 if !isWinner { 31 // await winner return 32 <-n.isCloseInvoked.Ch() 33 return 34 } 35 defer done.Done() 36 // isCloseInvoked.IsInvoked is true so new Send SendMany will return immediately 37 // - unsent count is therefore strictly decreasing by Get and send-thread 38 39 // handle deferred Close 40 if isRunningThread { 41 // a thread is running, if it does not exit, it’s deferred close 42 // - await static thread state, one of: send-block exit sends gets alert 43 var threadState NBChanTState 44 select { 45 // thread exit 46 case <-n.tcThreadExitAwaitable.Ch(): 47 // the thread exited 48 // if at end of items, executee close immediately 49 if n.unsentCount.Load() == 0 { 50 threadState = NBChanExit 51 } 52 53 // NBChanSendBlock NBChanAlert NBChanSends NBChanGets 54 case threadState = <-n.stateCh(): 55 } 56 57 // an always thread in NBChanAlert must be alerted 58 // - isCloseInvoked.IsInvoked prevents further alert wait 59 if threadState == NBChanAlert { 60 n.tcAlertThread() 61 } 62 63 // deferred close function 64 if threadState != NBChanExit { 65 return // deferred close 66 } 67 } 68 69 // immediate close 70 // - send-thread or Get to consume remaining items 71 for n.unsentCount.Load() > 0 { 72 <-n.updateDataAvailable() 73 n.getsWait.Wait() 74 } 75 76 didClose, _ = n.executeChClose() 77 // update datawaitCh 78 n.setDataAvailableAfterClose() 79 80 return 81 } 82 83 // CloseNow closes without waiting for sends to complete. 84 // - CloseNow is thread-safe, panic-free, idempotent, deferrable and is 85 // designed to not block for long 86 // - CloseNow does not return until the channel is closed and no thread is running 87 // - Upon return, errp and err receive any close or panic errors for this [NBChan] 88 // - if errp is non-nil, it is updated with error status 89 func (n *NBChan[T]) CloseNow(errp ...*error) (didClose bool, err error) { 90 91 // add any occuring errors for this [NBChan] 92 defer n.appendErrors(&err, errp...) 93 94 // select close now winner 95 var isWinner, isRunningThread, done, doneClose = n.selectCloseNowWinner() 96 if !isWinner { 97 <-n.isCloseNow.Ch() 98 return // close now loser threads 99 } 100 // this is CloseNow winner thread 101 defer done.Done() 102 if doneClose != nil { 103 defer doneClose.Done() 104 } 105 106 // wait for any Send SendMany Get to complete 107 // - Get Collect uses underlying channel 108 // - new invocations are canceled by: 109 // - — Send SendMany: isCloseInvoked.IsInvoked 110 // - — Get: isCloseNow.IsInvoked 111 n.getsWait.Wait() 112 n.sendsWait.Wait() 113 114 // release thread so it will exit 115 // - gets and sends is zero, so thread is not held up there 116 // - isCloseNow.IsInvoked is true 117 if isRunningThread { 118 // a thread was running when IsCloseNow activated 119 // - await a state for the thread 120 // - holding inputLock outputLock so no Send SendMany Get 121 var threadState NBChanTState 122 select { 123 // thread exit 124 case <-n.tcThreadExitAwaitable.Ch(): 125 // NBChanSendBlock NBChanAlert 126 case threadState = <-n.stateCh(): 127 } 128 129 switch threadState { 130 // alway thread awaiting alert 131 case NBChanAlert: 132 // alert and isclosenow will cause thread to exit 133 n.tcAlertThread() 134 // thread blocked in value send 135 case NBChanSendBlock: 136 select { 137 // discard any data item received from thread 138 // - thread will exit due to isCloseNow 139 case <-n.closableChan.Ch(): 140 // cancel on thread exit 141 case <-n.tcThreadExitAwaitable.Ch(): 142 } 143 } 144 } 145 146 // await thread exit 147 if isRunningThread { 148 <-n.tcThreadExitAwaitable.Ch() 149 } 150 151 // close data ch waiter 152 // - this will release a possible held Close invocation 153 n.setDataAvailableAfterClose() 154 155 // execute close 156 didClose, _ = n.executeChClose() 157 158 // discard pending data 159 n.outputLock.Lock() 160 defer n.outputLock.Unlock() 161 n.inputLock.Lock() 162 defer n.inputLock.Unlock() 163 164 // - thread has exited 165 // - Send SendMany Gets have ceased 166 // - this thread holds both locks 167 168 if nT := len(n.inputQueue); nT > 0 { 169 n.unsentCount.Add(uint64(-nT)) 170 } 171 n.inputQueue = nil 172 n.inputCapacity.Store(0) 173 if nT := len(n.outputQueue); nT > 0 { 174 n.unsentCount.Add(uint64(-nT)) 175 } 176 n.outputQueue = nil 177 n.outputCapacity.Store(0) 178 179 return 180 } 181 182 // selectCloseNowWinner ensures CloseNow execution 183 // - isWinner true: this thread executes close now 184 // - isRunningThread: a thread was running at time of close now/ 185 // if so, tcThreadExitAwaitable is armed 186 // - done: completion Done for winner thread 187 // - caller must hold inputLock: to update isCloseNow and isCloseInvoked 188 // - caller must hold inputLock and outputLock to prevent subsequent thread launch 189 func (n *NBChan[T]) selectCloseNowWinner() ( 190 isWinner, 191 isRunningThread bool, 192 done, doneClose Done, 193 ) { 194 n.tcThreadLock.Lock() // atomizes CloseNow true with tcRunningThread 195 defer n.tcThreadLock.Unlock() 196 197 // select CloseNow winner 198 if isWinner, done = n.isCloseNow.IsWinner(NoOnceWait); !isWinner { 199 return // CloseNow was completed by another thread 200 } 201 // is winning CloseNow thread 202 203 // CloseNow also signals Close 204 _, doneClose = n.isCloseInvoked.IsWinner(NoOnceWait) 205 206 // thread status at time of CloseNow 207 isRunningThread = n.tcRunningThread.Load() 208 209 return 210 } 211 212 // selectCloseWinner selects the winner to execuite close possily deferred to thread 213 // - executeCloseNow true: is winner thread and close is not deferred 214 // - deferred close is setting isCloseInvoked to true while a thread is running 215 // - caller must hold inputLock for isCloseInvoked update 216 func (n *NBChan[T]) selectCloseWinner() (isWinner, isRunningThread bool, done Done) { 217 // atomize close win with reading running thread-state 218 n.tcThreadLock.Lock() // atomizes Close:true tcRunningThread 219 defer n.tcThreadLock.Unlock() 220 221 var _ OnceCh 222 223 // select winner 224 // - losers do not wait here to get out of tcThreadLock 225 if isWinner, done = n.isCloseInvoked.IsWinner(NoOnceWait); !isWinner { 226 return // Close was already invoked return: executeCloseNow: false 227 } 228 // this thread is close winner 229 230 isRunningThread = n.tcRunningThread.Load() 231 232 return // if thread is not running: executeCloseNow: true: execute close now 233 } 234 235 // executeChClose closes the underlying channel 236 // - didClose true: this invocation closed the channel 237 // - err possible error, already submitted: unused 238 // - idempotent thread-safe 239 // - isCloseInvoked.IsInvoked must be true 240 // - unsent count must be zero 241 // - invoked by: 242 // - — CloseNow 243 // - — Close if not deferred close 244 // - — send thread in deferred close: on exit if Close was invoked prior to thread exit 245 func (n *NBChan[T]) executeChClose() (didClose bool, err error) { 246 247 // await Send SendMany ceasing 248 // - new invocations return immediately due to isCloseInvoked.IsInvoked true 249 n.sendsWait.Wait() 250 251 // wait for any Send SendMany Get to complete 252 // - Get Collect uses underlying channel 253 254 if didClose, err = n.closableChan.Close(); !didClose { 255 return // already closed return: noop 256 } else if err != nil { 257 n.AddError(err) // store possible close error 258 } 259 // update [NBChan.waitForClose] 260 n.waitForClose.Close() 261 262 return 263 } 264 265 // appendErrors aggregates any errors for this [NBChan] in any non-nil errp0 or errp 266 // - like perrors.AppendErrorDefer but allows for errp to be nil 267 func (n *NBChan[T]) appendErrors(errp0 *error, errp ...*error) { 268 269 if errp0 != nil { 270 perrors.AppendErrorDefer(errp0, nil, n.GetError) 271 } 272 // obtain error pointers 273 for _, errpx := range errp { 274 if errpx == nil { 275 continue 276 } 277 perrors.AppendErrorDefer(errpx, nil, n.GetError) 278 } 279 }