github.com/haraldrudell/parl@v0.4.176/period-waiter.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  
    12  // PeriodWaiter temporarily holds threads invoking Wait
    13  //   - HoldWaiters causes any Wait invokers to block until ReleaseWaiters
    14  //
    15  // that has not been succeeded by a ReleaseWaiters invocation. Thread-safe
    16  type PeriodWaiter struct {
    17  	// gen is initialized by HoldWaiters and set to nil by
    18  	// ReleaseWaiters
    19  	gen atomic.Pointer[periodWaiter]
    20  }
    21  
    22  type periodWaiter struct {
    23  	// waitGroup holds Wait invokers
    24  	wg WaitGroupCh
    25  	// number of threads in Wait
    26  	count atomic.Uint64
    27  }
    28  
    29  // NewPeriodWaiter returns an object that can hold threads temporarily. Thread-safe
    30  //   - initially [PeriodWaiter.Wait] invokers are not held
    31  //   - After invoking [PeriodWaiter.], Wait invokers are held until an invocation of
    32  //     [PeriodWaiter.ReleaseWaiters]
    33  func NewPeriodWaiter() (periodWaiter *PeriodWaiter) { return &PeriodWaiter{} }
    34  
    35  // HoldWaiters causes a thread invoking [PeriodWaiter.Wait] to wait. Thread-safe
    36  //   - idempotent
    37  func (p *PeriodWaiter) HoldWaiters() {
    38  
    39  	// check for already waiting
    40  	if p.gen.Load() != nil {
    41  		return // already waiting return: noop
    42  	}
    43  
    44  	// try to add a created wait group
    45  	var pw periodWaiter
    46  	// add one causing [WaitGroupCh.Wait] to block
    47  	pw.wg.Add(1)
    48  	// atomic nil to value of p.wg
    49  	p.gen.CompareAndSwap(nil, &pw)
    50  }
    51  
    52  // ReleaseWaiters releases any threads blocked in [PeriodWaiter.Wait]
    53  // and lets new Wait invokers proceed. Thread-safe
    54  func (p *PeriodWaiter) ReleaseWaiters() {
    55  
    56  	// atomic read of the current wait group
    57  	var pw = p.gen.Load()
    58  	if pw == nil {
    59  		return // state not waiting return: noop
    60  	}
    61  
    62  	// resolve any successfully obtained waitgroup
    63  	if !p.gen.CompareAndSwap(pw, nil) {
    64  		return // other thread already niled it: noop
    65  	}
    66  	pw.wg.Done()
    67  }
    68  
    69  // Count returns the number of threads currently in Wait
    70  //   - threads invoking [PeriodWaiter.Ch] or
    71  func (p *PeriodWaiter) Count() (waitingThreads int) {
    72  	if pw := p.gen.Load(); pw != nil {
    73  		waitingThreads = int(pw.count.Load())
    74  	}
    75  	return
    76  }
    77  
    78  // IsHold returns true if Wait will currently block
    79  func (p *PeriodWaiter) IsHold() (isHold bool) { return p.gen.Load() != nil }
    80  
    81  // Ch returns a channel that closes on ReleaseWaiters
    82  //   - ch is nil if not currently waiting
    83  func (p *PeriodWaiter) Ch() (ch AwaitableCh) {
    84  	if pw := p.gen.Load(); pw != nil {
    85  		ch = pw.wg.Ch()
    86  	}
    87  	return
    88  }
    89  
    90  // Wait blocks the thread if a HoldWaiters invocation took place with no
    91  // ReleaseWaiters succeeding it. Thread-safe
    92  func (p *PeriodWaiter) Wait() {
    93  
    94  	// keep waiting until the current wg value is nil
    95  	for {
    96  		var pw = p.gen.Load()
    97  		if pw == nil {
    98  			return // no mandated wait: noop return
    99  		}
   100  		pw.count.Add(1)
   101  		pw.wg.Wait()
   102  	}
   103  }