github.com/haraldrudell/parl@v0.4.176/pslices/range-ch.go (about)

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package pslices
     7  
     8  import (
     9  	"github.com/haraldrudell/parl/internal/cyclebreaker"
    10  )
    11  
    12  // RangeCh is a range-able channel based on a thread-safe slice
    13  //   - the slice is provided to the New function
    14  //   - each RangeCh has an internal thread that runs until:
    15  //   - — the slice is out of items
    16  //   - — Close is invoked
    17  //   - RangeCh.Ch() must be either read until close or RangeCh.Close must be invoked
    18  //   - Close discards items
    19  //   - —
    20  //   - it is unclear whether pairing an updatable slice with an unbuffered channel is useful
    21  //   - — unlike a buffered channel, RangeCh is unbound
    22  //   - — RangeCh costs 1 thread maintaining the unbuffered channel
    23  //   - — channels are somewhat inefficient by processing only one element at a time
    24  //   - — controlling allocations by processing single elements, slices of elements or
    25  //     slices of slices of elements
    26  //   - — RangeCh sends any available elements then closes the channel and exits
    27  type RangeCh[T any] struct {
    28  	// the range-able channel sending data
    29  	//	- closes on end-of-data
    30  	//	- unbuffered
    31  	//	- send and close by rangeChWriteThread
    32  	ch chan T
    33  	// isClosed indicates that [RangeCh.Close] has been invoked
    34  	//	- the channel ch is about to close
    35  	//	- isClosed.Close is idempotent
    36  	//	- synchronization mechanic to exit rangeChWriteThread
    37  	//	- observable
    38  	isClosed *cyclebreaker.Awaitable
    39  	// isExit is synchronization mechanic that rangeChWriteThread has exit
    40  	//	- on isExit closing, ch is already closed
    41  	isExit cyclebreaker.AwaitableCh
    42  }
    43  
    44  // NewRangeCh returns a range-able channel based on a [ThreadSafeSlice]
    45  //   - the Ch channel must be either read until it closes or Close must be invoked
    46  //   - Ch is a channel that can be used in a Go for-range clause
    47  //   - —
    48  //   - for can range over: array slice string map or channel
    49  //   - the only dynamic range source is channel which costs a thread sending on and closing the channel
    50  func NewRangeCh[T any](tss *ThreadSafeSlice[T]) (rangeChan *RangeCh[T]) {
    51  	var isExit = make(chan struct{})
    52  	r := RangeCh[T]{
    53  		ch:       make(chan T),
    54  		isClosed: cyclebreaker.NewAwaitable(),
    55  		isExit:   isExit,
    56  	}
    57  
    58  	// launch sending thread
    59  	go rangeChWriteThread(tss, r.ch, r.isClosed.Ch(), isExit)
    60  
    61  	return &r
    62  }
    63  
    64  // Ch returns the range-able channel sending values, thread-safe
    65  func (r *RangeCh[T]) Ch() (ch <-chan T) { return r.ch }
    66  
    67  // Close closes the ch channel, thread-safe, idempotent
    68  //   - Close may discard a pending item and causes the thread to stop reading from the slice
    69  //   - Close does not return until Ch is closed and the thread have exited
    70  func (r *RangeCh[T]) Close() {
    71  
    72  	// cause rangeChWriteThread to exit
    73  	r.isClosed.Close()
    74  
    75  	// wait for rangeChWriteThread to exit
    76  	<-r.isExit
    77  }
    78  
    79  // State returns [RangeCh] state
    80  // - isCloseInvoked is true if [RangeCh.Close] has been invoked, but Close may not have completed
    81  // - isChClosed means [RangeCh.Ch] has closed and resources are released
    82  // - exitCh allows to wait for Close complete
    83  func (r *RangeCh[T]) State() (isCloseInvoked, isChClosed bool, exitCh <-chan struct{}) {
    84  	isCloseInvoked = r.isClosed.IsClosed()
    85  	exitCh = r.isExit
    86  	select {
    87  	case <-exitCh:
    88  		isChClosed = true
    89  	default:
    90  	}
    91  	return
    92  }
    93  
    94  // rangeChWriteThread writes elements to r.ch until end or Close
    95  //   - typically rangeChWriteThread will block in channel send
    96  //   - infallible: runtime errors printed to standard error
    97  func rangeChWriteThread[T any](
    98  	tss *ThreadSafeSlice[T],
    99  	ch chan<- T,
   100  	isClosedCh cyclebreaker.AwaitableCh,
   101  	isExit chan struct{},
   102  ) {
   103  	var err error
   104  	defer cyclebreaker.Recover(func() cyclebreaker.DA { return cyclebreaker.A() }, &err, cyclebreaker.Infallible)
   105  	defer cyclebreaker.Closer(isExit, &err)
   106  	defer cyclebreaker.CloserSend(ch, &err)
   107  
   108  	var index int
   109  	for {
   110  
   111  		// get element to send
   112  		//	- non-blocking
   113  		//	- hasValue false when out of values
   114  		var element, hasValue = tss.Get(index)
   115  		if !hasValue {
   116  			return // end of items return
   117  		}
   118  		index++
   119  
   120  		// write thread normally blocks here
   121  		select {
   122  		case ch <- element:
   123  		case <-isClosedCh:
   124  			return // [RangeCh.Close] invoked
   125  		}
   126  	}
   127  }