github.com/haraldrudell/parl@v0.4.176/nb-rare-chan.go (about) 1 /* 2 © 2024–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package parl 7 8 import ( 9 "sync" 10 "sync/atomic" 11 12 "github.com/haraldrudell/parl/perrors" 13 ) 14 15 // NBRareChan is a simplified [NBChan] using on-demand thread 16 // - NBRareChan is a channel with unbound buffer 17 // like an unbuffered channel reading from a thread-safe slice 18 // - [NBRareChan.Send] provides value send that is non-blocking-send, thread-safe, panic, deadlock and error-free 19 // - [NBRareChan.Ch] provides real-time value stream or 20 // - [NBRareChan.Close] provides all buffered values 21 // - [NBRareChan.StopSend] blocks further Send allowing for graceful shutdown 22 // - [NBRareChan.IsClose] returns whether the underlying channel is closed 23 // - [NBRareChan.PanicCh] provides real-time notice of thread panic, should not happen 24 // - NBRareChan is initialization-free and thread-safe with 25 // thread-safe panic-free idempotent observable deferrable Close 26 // - used as an error sink, NBRareChan[error] prevents error propagation from affecting the thread 27 // - ignoring thread panics and Close errp is reasonably safe simplification 28 // - intended for infrequent use such as an error sink 29 // - benefits over plain channel: 30 // - — [NBRareChan.Send] is non-blocking-send panic-free non-dead-locking 31 // - — initialization-free 32 // - — unbound buffer 33 // - — any thread can close the channel as opposed to only the sending thread 34 // - — thread-synchronizing unbuffered channel-send mechanic as opposed to a buffered channel 35 // - — graceful shutdown like a buffered channel 36 // - — [NBRareChan.Close] is any-thread data-race-free thread-safe panic-free idempotent observable deferrable 37 // - drawbacks compared to [NBChan]: 38 // - — on-demand thread may lead to high cpu if used frequently like every second. 39 // NBChan offers no-thread or always-thread operation 40 // - — buffering a large number of items leads to a temporary memory leak in queue 41 // - — there is no contention-separation between Send and reading Ch 42 // - — no multiple-item operations like SendMany or Get 43 // - — less observable and configurable 44 // - see also: 45 // - — [NBChan] fully-featured unbound channel 46 // - — [AwaitableSlice] unbound awaitable queue 47 // 48 // Deprecated: NBRareChan is replaced by [github.com/haraldrudell/parl.AwaitableSlice] for performance and 49 // efficiency reasons. [github.com/haraldrudell/parl.ErrSlice] is an error container implementation 50 type NBRareChan[T any] struct { 51 // underlying channel 52 // - closed by first Close invocation 53 closableChan ClosableChan[T] 54 // threadWait makes all created send threads awaitable 55 threadWait sync.WaitGroup 56 // queueLock ensures thread-safety of queue 57 // - also ensures sequenced access to isThread isStopSend 58 queueLock sync.Mutex 59 // didSend idicates that Send did create a send-thread 60 // whose value may need to be collected on Close 61 // - behind queueLock 62 didSend bool 63 // queue is a slice-away slice of unsent data 64 // - Send appends to queue 65 // - behind queueLock 66 queue []T 67 // threadReadingValues indicates that a send thread is 68 // currently reading values from queue 69 // - accessed behind queueLock 70 threadReadingValues CyclicAwaitable 71 // accessed behind queueLock 72 isStopSend atomic.Bool 73 // returned by StopSend await empty channel 74 isEmpty Awaitable 75 // sendThread panics, should be none 76 errs atomic.Pointer[error] 77 // isPanic indicates that a send thread suffered a panic 78 // - triggers PanicCh awaitable 79 isPanic Awaitable 80 // ensures close executed once 81 closeOnce OnceCh 82 } 83 84 // Ch obtains the underlying channel for channel receive operations 85 func (n *NBRareChan[T]) Ch() (ch <-chan T) { return n.closableChan.Ch() } 86 87 // Send sends a single value on the channel 88 // - non-blocking-send, thread-safe, deadlock-free, panic-free and error-free 89 // - if Close or StopSend was invoked, value is discarded 90 func (n *NBRareChan[T]) Send(value T) { 91 n.queueLock.Lock() 92 defer n.queueLock.Unlock() 93 94 // ignore values after Close or StopSend 95 if n.closableChan.IsClosed() || n.isStopSend.Load() { 96 return 97 } 98 99 // possibly create thread with value 100 var createThread bool 101 if createThread = !n.didSend; createThread { 102 n.didSend = true 103 } else { 104 createThread = n.threadReadingValues.IsClosed() 105 } 106 if createThread { 107 n.threadReadingValues.Open() 108 n.threadWait.Add(1) 109 go n.sendThread(value) 110 return 111 } 112 113 // append value to buffer 114 n.queue = append(n.queue, value) 115 } 116 117 // StopSend ignores further Send allowing for the channel to be drained 118 // - emptyAwaitable triggers once the channel is empty 119 func (n *NBRareChan[T]) StopSend() (emptyAwaitable AwaitableCh) { 120 n.queueLock.Lock() 121 defer n.queueLock.Unlock() 122 123 n.isStopSend.CompareAndSwap(false, true) 124 emptyAwaitable = n.isEmpty.Ch() 125 if len(n.queue) == 0 && n.threadReadingValues.IsClosed() { 126 n.isEmpty.Close() 127 } 128 return 129 } 130 131 // PanicCh is real-time awaitable for panic in sendThread 132 // - this should not happen 133 func (n *NBRareChan[T]) PanicCh() (emptyAwaitable AwaitableCh) { return n.isPanic.Ch() } 134 135 // IsClose returns true if underlying channel is closed 136 func (n *NBRareChan[T]) IsClose() (isClose bool) { return n.closableChan.IsClosed() } 137 138 // Close immediately closes the channel returning any unsent values 139 // - values: possible values that were in channel, may be nil 140 // - errp: receives any panics from thread. Should be none. may be nil 141 // - upon return, resources are released and further Send ineffective 142 func (n *NBRareChan[T]) Close(values *[]T, errp *error) { 143 144 // ensure once execution 145 if isWinner, done := n.closeOnce.IsWinner(); !isWinner { 146 return // loser thread has already awaited done 147 } else { 148 defer done.Done() 149 } 150 151 // collect queue and stop further Send 152 var queue = n.close() 153 154 // collect possible value from thread and shut it down 155 if n.didSend { 156 select { 157 case value := <-n.closableChan.Ch(): 158 queue = append([]T{value}, queue...) 159 case <-n.threadReadingValues.Ch(): 160 n.isEmpty.Close() 161 } 162 // wait for all created threads to exit 163 n.threadWait.Wait() 164 if errp != nil { 165 if ep := n.errs.Load(); ep != nil { 166 *errp = perrors.AppendError(*errp, *ep) 167 } 168 } 169 } 170 171 // close underlying channel 172 n.closableChan.Close() 173 174 // return values 175 if values != nil && len(queue) > 0 { 176 *values = queue 177 } 178 } 179 180 // collect queue and stop further Send 181 func (n *NBRareChan[T]) close() (values []T) { 182 n.queueLock.Lock() 183 defer n.queueLock.Unlock() 184 185 if values = n.queue; values != nil { 186 n.queue = nil 187 } 188 n.isStopSend.Store(true) 189 190 return 191 } 192 193 // sendThread carries out send operations on the channel 194 func (n *NBRareChan[T]) sendThread(value T) { 195 defer n.threadWait.Done() 196 defer Recover(func() DA { return A() }, nil, n.sendThreadPanic) 197 198 var ch = n.closableChan.Ch() 199 for { 200 ch <- value 201 var hasValue bool 202 if value, hasValue = n.sendThreadNextValue(); !hasValue { 203 return 204 } 205 } 206 } 207 208 // sendThreadNextValue obtains the next value to send for thread if any 209 func (n *NBRareChan[T]) sendThreadNextValue() (value T, hasValue bool) { 210 n.queueLock.Lock() 211 defer n.queueLock.Unlock() 212 213 if hasValue = len(n.queue) > 0; hasValue { 214 value = n.queue[0] 215 n.queue = n.queue[1:] 216 return 217 } 218 // channel detected empty 219 220 // - notify that sendThread is no longer awaiting values 221 n.threadReadingValues.Close() 222 223 // if StopSend was invoked, notify its awaitable 224 if n.isStopSend.Load() { 225 n.isEmpty.Close() 226 } 227 228 return 229 } 230 231 // sendThreadPanic aggregates thread panics 232 func (n *NBRareChan[T]) sendThreadPanic(err error) { 233 234 // provide panic error to errs 235 for { 236 var errp0 = n.errs.Load() 237 if errp0 == nil && n.errs.CompareAndSwap(nil, &err) { 238 break // wrote new error 239 } 240 var err2 = perrors.AppendError(*errp0, err) 241 if n.errs.CompareAndSwap(errp0, &err2) { 242 break // appended error 243 } 244 } 245 246 // notify awaitable that a panic occured 247 n.isPanic.Close() 248 n.queueLock.Lock() 249 defer n.queueLock.Unlock() 250 251 n.threadReadingValues.Close() 252 }