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  }