github.com/haraldrudell/parl@v0.4.176/nb-chan-thread.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  	"math"
    10  
    11  	"github.com/haraldrudell/parl/pruntime"
    12  )
    13  
    14  // sendThread feeds values to the send channel
    15  //   - may be on-demand or always-on thread
    16  //   - verbose='NBChan.*sendThread'
    17  func (n *NBChan[T]) sendThread(value T, hasValue bool) {
    18  	var zeroValue T
    19  	// signal thread exit to CloseNow if it is waiting
    20  	defer n.tcThreadExitAwaitable.Close()
    21  	// execute possible deferred close from Close invocation
    22  	defer n.sendThreadDeferredClose()
    23  	defer Recover(func() DA { return A() }, nil, n.sendThreadOnError)
    24  
    25  	// send value loop
    26  	for {
    27  
    28  		if hasValue {
    29  			// send the value: blocks here
    30  			//	- until consumer receive, Get, CloseNow or panic
    31  			//	- decrements unsent count
    32  			n.sendThreadBlockingSend(value)
    33  			hasValue = false
    34  			value = zeroValue
    35  		}
    36  
    37  		// obtain next value loop
    38  		for {
    39  
    40  			// check for CloseNow prior to next value
    41  			if n.sendThreadIsCloseNow() {
    42  				return // close now exit: immediate discard and exit
    43  			}
    44  
    45  			// if no data, decide on action
    46  			if n.unsentCount.Load() == 0 {
    47  				n.sendThreadZero()
    48  
    49  				// always-thread not in deferred close: wait for alert
    50  				if !n.isOnDemandThread.Load() && !n.isCloseInvoked.IsInvoked() {
    51  					// blocks here
    52  					if value, hasValue = n.sendThreadWaitForAlert(); hasValue {
    53  						break // send the value received by alert
    54  					}
    55  					continue // re-check closeNow and unsent count for next action
    56  
    57  					// on-demand thread or always in deferred close:
    58  					// exit on no data and no pending sends
    59  				} else if n.sendThreadExitCheck() {
    60  					// on-demand thread or always-on after Close exits here
    61  					return // no data, no pending sends: exit thread
    62  				}
    63  			} // obtain next value loop
    64  			// there is more data to send
    65  
    66  			if hasValue {
    67  				break // send the always-on thread value from alert
    68  			}
    69  			// is on-demand thread and data is available
    70  
    71  			// wait for [NBChan.gets] to be or reach 0
    72  			//	- Get invocations get items before sendThread
    73  			if ch := n.getsWait.Ch(); ch != nil {
    74  				for {
    75  					select {
    76  					case <-ch: // Get ceased
    77  					case n.stateCh() <- NBChanGets: // respond is in Gets wait
    78  						continue
    79  					}
    80  					break
    81  				}
    82  			}
    83  
    84  			// try to get value from any queue
    85  			if value, hasValue = n.sendThreadGetNextValue(); hasValue {
    86  				break // send the value fetched from queues
    87  			}
    88  
    89  			// unsent count has reached zero or Get is in progress
    90  			//	- wait for any sends to conclude that may provide additional items
    91  			if ch := n.sendsWait.Ch(); ch != nil {
    92  				for {
    93  					select {
    94  					case <-ch:
    95  					case n.stateCh() <- NBChanSends:
    96  						continue
    97  					}
    98  					break
    99  				}
   100  			}
   101  		}
   102  		// a value was obtained
   103  
   104  		// only send if closeNow has not been invoked
   105  		if !n.sendThreadNewSendCheck() {
   106  			return // CloseNow: item is discarded, thread exits
   107  		}
   108  	}
   109  }
   110  
   111  // sendThreadDeferredClose may close the underlying channel
   112  //   - is how sendThread executes deferred close
   113  //   - closes if Close was invoked while thread running and not CloseNow
   114  //   - invoked by sendThread on exit
   115  //   - updates dataWaitCh
   116  func (n *NBChan[T]) sendThreadDeferredClose() {
   117  
   118  	// is it deferred close?
   119  	if !n.isCloseInvoked.IsInvoked() || // no: Close has not been invoked
   120  		n.isCloseNow.IsInvoked() { // CloseNow overrides deferred close
   121  		n.updateDataAvailable()
   122  		return // no deferred close pending return: noop
   123  	}
   124  
   125  	// for on-demand thread, ensure out of data
   126  	if n.isOnDemandThread.Load() {
   127  
   128  		if n.unsentCount.Load() > 0 {
   129  			return
   130  		}
   131  		// tcThread
   132  	}
   133  
   134  	// execute deferred close
   135  	//	- error is stored in error container. isClosed is active
   136  	n.executeChClose()
   137  	// close data waiter
   138  	n.setDataAvailableAfterClose()
   139  }
   140  
   141  // sendThreadZero notifies background that thread
   142  // took action on unsent count zero
   143  func (n *NBChan[T]) sendThreadZero() {
   144  	n.tcDoProgressRaised(true)
   145  	n.tcProgressLock.Lock()
   146  	defer n.tcProgressLock.Unlock()
   147  
   148  	if n.unsentCount.Load() == 0 {
   149  		n.tcProgressRequired.Store(true)
   150  	}
   151  }
   152  
   153  // sendThreadOnError submits thread panic
   154  //   - ignores send on closed channel after closenow
   155  func (n *NBChan[T]) sendThreadOnError(err error) {
   156  	if pruntime.IsSendOnClosedChannel(err) && n.isCloseNow.IsInvoked() {
   157  		return // ignore if the channel was or became closed
   158  	}
   159  	n.AddError(err)
   160  }
   161  
   162  // sendThreadBlockingSend sends blocking on consumer-receive channel
   163  //   - decrements unsent count
   164  //   - blocks until:
   165  //   - — consumer read the value
   166  //   - — Get empties the channel using collectSendThreadValue
   167  //   - — CloseNow discards the value using discardSendThreadValue
   168  //   - invoked by sendThread holding inputLock
   169  func (n *NBChan[T]) sendThreadBlockingSend(value T) {
   170  	defer n.updateDataAvailable()
   171  	// count the item just sent — even if panic
   172  	defer n.unsentCount.Add(math.MaxUint64)
   173  	// clear two-chan receive second channel
   174  	n.collectChanActive.Store(nil)
   175  	// receive value with default has proven to result in default. Therefore:
   176  	//	- two-chan receive is used by tcCollectThreadValue to prevent deadlock and aba
   177  	//	- send-thread provides an atomic true and a nil atomic channel value
   178  	//		upon commencing send operation
   179  	//	- the atomic true allows other threads to write the atomic channel value and
   180  	//		reset the atomic true to false
   181  	//	- a winner thread observing the atomic true value stores a 1-size empty channel,
   182  	//		and proceeds if it is able to set the atomic true value to false
   183  	//	- at end of send operation, send-thread attempts to change the atomic value from true to false
   184  	//	- if the atomic value was true, no two-chan receive is in progress
   185  	//	- otherwise, send-thread sends on the atomic channel
   186  	//	- thereby, send-thread will not enter dead-lock and avoids aba-issue
   187  	defer n.sendThreadBlockingSendEnd()
   188  	// tcSendBlock makes collectChanActive available to tcCollectThreadValue threads
   189  	n.tcSendBlock.Store(true)
   190  
   191  	for {
   192  		select {
   193  		// send value to consumer or Get
   194  		//	- may block or panic
   195  		case n.closableChan.Ch() <- value:
   196  			return
   197  		case n.stateCh() <- NBChanSendBlock:
   198  		}
   199  	}
   200  }
   201  
   202  // sendThreadBlockingSendEnd completes any two-chan receive operation
   203  func (n *NBChan[T]) sendThreadBlockingSendEnd() {
   204  	// check if two-chan send was initiated
   205  	if n.tcSendBlock.CompareAndSwap(true, false) {
   206  		return // no value collect
   207  	}
   208  
   209  	// send to ensure tcCollectThreadValue is not blocked
   210  	if cp := n.collectChanActive.Load(); cp != nil {
   211  		*cp <- struct{}{}
   212  	}
   213  }
   214  
   215  // sendThreadGetNextValue gets the next value for thread
   216  //   - invoked by [NBChan.sendThread]
   217  //   - fails if pending Get or unsentCount ends up zero
   218  func (n *NBChan[T]) sendThreadGetNextValue() (value T, hasValue bool) {
   219  	if n.gets.Load() > 0 || n.unsentCount.Load() == 0 {
   220  		return // send thread suspended by Get return: hasValue: false
   221  	}
   222  	// if a thread holding outputLock awaited thread state,
   223  	// acquiring outputLock here could cause dead-lock
   224  	//	- only Get invocations do this
   225  	//	- therefore, ensure outputLock is not acquired while Get
   226  	//		in progress
   227  	n.collectorLock.Lock()
   228  	defer n.collectorLock.Unlock()
   229  
   230  	if n.gets.Load() > 0 {
   231  		return // cancel: Get in progress
   232  	}
   233  	n.outputLock.Lock()
   234  	defer n.outputLock.Unlock()
   235  
   236  	if hasValue = len(n.outputQueue) > 0 || n.swapQueues(); !hasValue {
   237  		return // no value available return: hasValue false
   238  	}
   239  	value = n.outputQueue[0]
   240  	n.outputQueue = n.outputQueue[1:]
   241  	return // have item return: value: valid, hasValue: true
   242  }
   243  
   244  // sendThreadWaitForAlert allows an always-on thread to await alert
   245  //   - the alert is a two-chan send that may provide a value
   246  //   - always threads do not exit, instead at end of data
   247  //     they wait for background events:
   248  //   - not if didClose
   249  //   - not if data available
   250  //   - an alert that may provide a data item
   251  func (n *NBChan[T]) sendThreadWaitForAlert() (value T, hasValue bool) {
   252  
   253  	// reset atomic channel
   254  	n.alertChan2Active.Store(nil)
   255  	// n.threadChWinner true exposes channels to clients
   256  	n.tcAlertActive.Store(true)
   257  	// sending on threadCh2 ensures no client is hanging
   258  	defer n.sendThreadAlertEnd()
   259  
   260  	// blocks here
   261  	//	- n.threadCh must be unbuffered for effect to be immediate
   262  	//	- n.threadCh2 is present to prevent client from hanging in threadCh send
   263  	for {
   264  		select {
   265  		// wait for alert
   266  		case valuep := <-n.alertChan.Get():
   267  			if hasValue = valuep != nil; hasValue {
   268  				value = *valuep
   269  				n.unsentCount.Add(1)
   270  			}
   271  			return
   272  			// broadcast Alert wait
   273  		case n.stateCh() <- NBChanAlert:
   274  		}
   275  	}
   276  }
   277  
   278  func (n *NBChan[T]) sendThreadAlertEnd() {
   279  	// see if two-chan send operation in progress
   280  	if n.tcAlertActive.CompareAndSwap(true, false) {
   281  		return // no
   282  	} else if cp := n.alertChan2Active.Load(); cp != nil {
   283  		*cp <- struct{}{}
   284  	}
   285  }
   286  
   287  // sendThreadExitCheck stops thread if inside threadLock, unsentCount is 0
   288  //   - doStop true: channel has been read to end and no Send SendMany active
   289  //   - invoked when thread has detected Close invocation and is in deferred close
   290  func (n *NBChan[T]) sendThreadExitCheck() (doStop bool) {
   291  	n.tcThreadLock.Lock() // atomizes unsentCount sends tcRunningThread
   292  	defer n.tcThreadLock.Unlock()
   293  
   294  	if doStop = //
   295  		n.unsentCount.Load() == 0 && // only stop if out of data
   296  			n.sends.Load() == 0; // while no sends in progress
   297  	!doStop {
   298  		return
   299  	}
   300  
   301  	n.tcRunningThread.Store(false)
   302  
   303  	return
   304  }
   305  
   306  // sendThreadNewSendCheck ensures a new channel send does not start after
   307  // CloseNow
   308  //   - on closeNow, value is discarded
   309  //   - re-arms closesOnThreadSend
   310  func (n *NBChan[T]) sendThreadNewSendCheck() (doSend bool) {
   311  
   312  	if doSend = !n.isCloseNow.IsInvoked(); !doSend {
   313  		n.unsentCount.Add(math.MaxUint64) // drop the value
   314  		return                            // CloseNow inoked return: doSend: false
   315  	}
   316  
   317  	return // doSend: true
   318  }
   319  
   320  // sendThreadIsCloseNow checks for CloseNow invocation
   321  //   - isExit true: CloseNow was invoked
   322  func (n *NBChan[T]) sendThreadIsCloseNow() (isExit bool) {
   323  	if !n.isCloseNow.IsInvoked() {
   324  		return // no CloseNow invocation return
   325  	}
   326  	n.tcThreadLock.Lock() // atomizes CloseNow tcRunningThread
   327  	defer n.tcThreadLock.Unlock()
   328  
   329  	if isExit = n.isCloseNow.IsInvoked(); !isExit {
   330  		return
   331  	}
   332  	n.tcRunningThread.Store(false)
   333  	return // close now exit
   334  }