github.com/haraldrudell/parl@v0.4.176/counting-awaitable.go (about)

     1  /*
     2  © 2023-present Harald Rudell <haraldrudell@proton.me> (https://haraldrudell.github.io/haraldrudell/)
     3  All rights reserved
     4  */
     5  
     6  package parl
     7  
     8  import (
     9  	"sync/atomic"
    10  
    11  	"github.com/haraldrudell/parl/perrors"
    12  )
    13  
    14  const (
    15  	bTrigBit     = 0x1
    16  	bNegativeBit = 0x2
    17  	bShift       = 2
    18  )
    19  
    20  type CountingAwaitable struct {
    21  	Awaitable
    22  	counter atomic.Int64
    23  	adds    atomic.Uint64
    24  }
    25  
    26  // NewCountingAwaitable returns a counting awaitable
    27  //   - similar to sync.WaitGroup but wait-mechanic is closing channel
    28  //   - observable via Count method.
    29  //     Count with missing delta will not panic
    30  //   - —
    31  //   - state is determined by counter.
    32  //     Guaranteed state is returned by Count IsTriggered
    33  //   - IsClosed is eventually consistent.
    34  //     A parallel Count may reflect triggered prior IsClose returning true.
    35  //     Upon return from the effective Count Add Done IsTriggered invocation,
    36  //     IsClose is consistent and the channel is closed if it is to close.
    37  //     For race-sensitive code, synchronize or rely on Count
    38  //   - similarly, a parallel Count invocation may reflect triggered state
    39  //     prior to the Awaitable channel actually closing
    40  //   - mechanic is atomic.Int64.CompareAndSwap
    41  func NewCountingAwaitable() (awaitable *CountingAwaitable) {
    42  	return &CountingAwaitable{}
    43  }
    44  
    45  // IsTriggered returns true if counter has been used and returned to zero
    46  //   - IsClosed true, AwaitableCh closed
    47  func (a *CountingAwaitable) IsTriggered() (isTriggered bool) {
    48  	return a.counter.Load()&bTrigBit != 0
    49  }
    50  
    51  // Count returns the current count and may also adjust the counter
    52  //   - counter is current remaining count
    53  //   - adds is cumulative positive adds
    54  //   - if delta is present and the awaitable is triggered, this is panic
    55  //   - state can be retrieved without panic by omitting delta
    56  //   - if delta is negative and result is zero: trig
    57  //   - if delta is negative and result is negative: panic
    58  //   - similar to sync.WaitGroup.Add Done
    59  func (a *CountingAwaitable) Count(delta ...int) (counter, adds int) {
    60  	var hasDelta = len(delta) > 0
    61  	if !hasDelta {
    62  		counter = int(a.counter.Load() >> bShift)
    63  		adds = int(a.adds.Load())
    64  		return
    65  	}
    66  	var delta0 = delta[0]
    67  
    68  	// thread-safe add to counter
    69  	var counter64 int64
    70  	for {
    71  		var value = a.counter.Load()
    72  		if value&bNegativeBit != 0 {
    73  			panic(perrors.NewPF("negative fail-state encountered"))
    74  		} else if value&bTrigBit != 0 {
    75  			panic(perrors.NewPF("Add or Done on triggered CountingAwaitable"))
    76  		}
    77  		if delta0 == 0 {
    78  			counter = int(value >> bShift)
    79  			adds = int(a.adds.Load())
    80  			return // noop
    81  		}
    82  		counter64 = value + int64(delta0)<<bShift
    83  		if counter64 == 0 {
    84  			counter64 |= bTrigBit
    85  		} else if counter < 0 {
    86  			counter |= bNegativeBit
    87  		}
    88  		if a.counter.CompareAndSwap(value, counter64) {
    89  			break // add succeeded
    90  		}
    91  	}
    92  	counter = int(counter64 >> bShift)
    93  	if delta0 > 0 {
    94  		adds = int(a.adds.Add(uint64(delta0)))
    95  	} else {
    96  		adds = int(a.adds.Load())
    97  	}
    98  	if counter64 != bTrigBit && counter > 0 {
    99  		return // no trig or negative
   100  	} else if counter64 < 0 {
   101  		panic(perrors.NewPF("negative counter"))
   102  	}
   103  
   104  	// trig!
   105  	if !a.Close() {
   106  		panic(perrors.NewPF("Awaitable already triggered"))
   107  	}
   108  
   109  	return
   110  }
   111  
   112  // Add signals thread-launch by adding to the counter
   113  //   - if delta is negative and result is zero: trig
   114  //   - if delta is negative and result is negative: panic
   115  //   - Add on triggered or negative is panic
   116  //   - similar to sync.WaitGroup.Add
   117  func (a *CountingAwaitable) Add(delta int) { a.Count(delta) }
   118  
   119  // Done signals thread exit by decrementing the counter
   120  //   - if delta is negative and result is zero: trig
   121  //   - if delta is negative and result is negative: panic
   122  //   - Add on triggered or negative is panic
   123  //   - similar to sync.WaitGroup.Done
   124  func (a *CountingAwaitable) Done() { a.Count(-1) }