github.com/haraldrudell/parl@v0.4.176/closable-chan.go (about) 1 /* 2 © 2022–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/atomic" 10 11 "github.com/haraldrudell/parl/perrors" 12 ) 13 14 // ClosableChan wraps a channel with thread-safe idempotent panic-free observable close. 15 // - ClosableChan is initialization-free 16 // - Close is deferrable 17 // - IsClosed provides wether the channel is closed 18 // 19 // Usage: 20 // 21 // var errCh parl.ClosableChan[error] 22 // go thread(&errCh) 23 // err, ok := <-errCh.Ch() 24 // if errCh.IsClosed() { // can be inspected 25 // … 26 // 27 // func thread(errCh *parl.ClosableChan[error]) { 28 // var err error 29 // … 30 // defer errCh.Close(&err) // will not terminate the process 31 // errCh.Ch() <- err 32 type ClosableChan[T any] struct { 33 // ch0 is the channel object 34 // - ability to initialize ch0 in the constructor 35 // - ability to update ch0 after creation 36 // - ch0 therefore must be pointer 37 // - ch0 must offer thread-safe access and update 38 39 // ch0 as provided by contructor or nil 40 ch0 chan T 41 // ch0 provided post-constructor because ch0 nil 42 chp atomic.Pointer[chan T] 43 44 // indicates the channel about to close or closed 45 // - because the channel may transfer data, it cannot be inspected for being closed 46 isCloseInvoked atomic.Bool 47 // [parl.Once] is an observable sync.Once 48 // - caches close result 49 // - provides atomic-performance done-flag 50 // - ensures no return prior to channel close complete 51 // - ensures exactly one close invocation 52 closeOnce Once 53 } 54 55 // NewClosableChan returns a channel with idempotent panic-free observable close 56 // - ch is an optional non-closed channel object 57 // - if ch is not present, an unbuffered channel will be created 58 // - cannot use lock in new function 59 // - if an unbuffered channel is used, NewClosableChan is not required 60 func NewClosableChan[T any](ch ...chan T) (closable *ClosableChan[T]) { 61 var ch0 chan T 62 if len(ch) > 0 { 63 ch0 = ch[0] // if ch is present, apply it 64 } 65 return &ClosableChan[T]{ch0: ch0} 66 } 67 68 // Ch retrieves the channel as bi-directional. Thread-safe 69 // - nil is never returned 70 // - the channel may be closed, use IsClosed to determine 71 // - do not close the channel other than using Close method 72 // - per Go channel close, if one thread is blocked in channel send 73 // while another thread closes the channel, a data race occurs 74 // - thread-safe solution is to set an additional indicator of 75 // close requested and then reading the channel which 76 // releases the sending thread 77 func (c *ClosableChan[T]) Ch() (ch chan T) { 78 return c.getCh() 79 } 80 81 // ReceiveCh retrieves the channel as receive-only. Thread-safe 82 // - nil is never returned 83 // - the channel may already be closed 84 func (c *ClosableChan[T]) ReceiveCh() (ch <-chan T) { 85 return c.getCh() 86 } 87 88 // SendCh retrieves the channel as send-only. Thread-safe 89 // - nil is never returned 90 // - the channel may already be closed 91 // - do not close the channel other than using the Close method 92 // - per Go channel close, if one thread is blocked in channel send 93 // while another thread closes the channel, a data race occurs 94 // - thread-safe solution is to set an additional indicator of 95 // close requested and then reading the channel which 96 // releases the sending thread 97 func (c *ClosableChan[T]) SendCh() (ch chan<- T) { 98 return c.getCh() 99 } 100 101 // IsClosed indicates whether the channel is closed. Thread-safe 102 // - includePending: because there is a small amount of time between 103 // - — a thread discovering the channel closed and 104 // - — closeOnce indicating close complete 105 // - includePending true includes a check for the channel being about 106 // to close 107 func (c *ClosableChan[T]) IsClosed(includePending ...bool) (isClosed bool) { 108 if len(includePending) > 0 && includePending[0] { 109 return c.isCloseInvoked.Load() 110 } 111 return c.closeOnce.IsDone() 112 } 113 114 // Close ensures the channel is closed 115 // - Close does not return until the channel is closed 116 // - all invocations have the same close result in err 117 // - didClose indicates whether this invocation closed the channel 118 // - if errp is non-nil, it will receive the close result 119 // - per Go channel close, if one thread is blocked in channel send 120 // while another thread closes the channel, a data race occurs 121 // - thread-safe, panic-free, deferrable, idempotent, observable 122 // - Close does not feature deferred close indication 123 // - — caller must ensure no channel send is in progress 124 // - — channel send after Close will fail 125 // - — a buffered channel can be read to empty after Close 126 func (cl *ClosableChan[T]) Close(errp ...*error) (didClose bool, err error) { 127 128 // ensure isCloseInvoked true: channel is about to close 129 cl.isCloseInvoked.CompareAndSwap(false, true) 130 131 // hasResult indicates that close did already complete 132 // and err was obtained with atomic performance 133 var hasResult bool 134 _, hasResult, err = cl.closeOnce.Result() 135 136 // first invocation closes the channel 137 // - subsequent invocations await close complete 138 // and return the close result 139 if !hasResult { 140 didClose, _, err = cl.closeOnce.DoErr(cl.doClose) 141 } 142 143 // update errp if present 144 if len(errp) > 0 { 145 if errp0 := errp[0]; errp0 != nil { 146 *errp0 = perrors.AppendError(*errp0, err) 147 } 148 } 149 150 return 151 } 152 153 // getCh gets or initializes the channel object [ClosableChan.ch] 154 func (c *ClosableChan[T]) getCh() (ch chan T) { 155 if ch = c.ch0; ch != nil { 156 return // channel from constructor return 157 } 158 for { 159 if chp := c.chp.Load(); chp != nil { 160 ch = *chp 161 return // chp was present return 162 } 163 if ch == nil { 164 ch = make(chan T) 165 } 166 if c.chp.CompareAndSwap(nil, &ch) { 167 return // chp updated return 168 } 169 } 170 } 171 172 // doClose is behind [ClosableChan.closeOnce] and 173 // is therefore only invoked once 174 // - separate function because provided to Once 175 func (cl *ClosableChan[T]) doClose() (err error) { 176 177 // ensure a channel exists and close it 178 Closer(cl.getCh(), &err) 179 180 return 181 }