github.com/haraldrudell/parl@v0.4.176/atomic-max.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  	"sync/atomic"
    10  
    11  	"github.com/haraldrudell/parl/ints"
    12  	"golang.org/x/exp/constraints"
    13  )
    14  
    15  // AtomicMax is a thread-safe max container
    16  //   - hasValue indicator true if a value was equal to or greater than threshold
    17  //   - optional threshold for minimum accepted max value
    18  //   - generic for any basic or named Integer type
    19  //   - negative values cause panic
    20  //   - can used to track maximum [time.Duration] that should never be negative
    21  //   - if threshold is not used, initialization-free
    22  //   - —
    23  //   - wait-free CompareAndSwap mechanic
    24  type AtomicMax[T constraints.Integer] struct {
    25  	// threshold is an optional minimum value for a new max
    26  	threshold uint64
    27  	// value is current max
    28  	value atomic.Uint64
    29  	// whether [AtomicMax.Value] has been invoked
    30  	// with value equal or greater to threshold
    31  	hasValue atomic.Bool
    32  }
    33  
    34  // NewAtomicMax returns a thread-safe max container
    35  //   - T any basic or named type with underlying type integer
    36  //   - negative values not allowed and cause panic
    37  //   - if threshold is not used, AtomicMax is initialization-free
    38  func NewAtomicMax[T constraints.Integer](threshold T) (atomicMax *AtomicMax[T]) {
    39  	m := AtomicMax[T]{}
    40  	if threshold != 0 {
    41  		m.threshold = m.tToUint64(threshold)
    42  	}
    43  	return &m
    44  }
    45  
    46  // Value updates the container with a possible max value
    47  //   - value cannot be negative, that is panic
    48  //   - isNewMax is true if:
    49  //   - — value is equal to or greater than any threshold and
    50  //   - — invocation recorded the first 0 or
    51  //   - — a new max
    52  //   - upon return, Max and Max1 are guaranteed to reflect the invocation
    53  //   - the return order of concurrent Value invocations is not guaranteed
    54  //   - Thread-safe
    55  func (m *AtomicMax[T]) Value(value T) (isNewMax bool) {
    56  
    57  	// check value against threshold
    58  	//	- because no negative values, comparison can be in uint64
    59  	var valueU64 = m.tToUint64(value)
    60  	if valueU64 < m.threshold {
    61  		return // below threshold return: isNewMax false
    62  	}
    63  
    64  	// 0 as max case
    65  	var hasValue0 = m.hasValue.Load()
    66  	if valueU64 == 0 {
    67  		if !hasValue0 {
    68  			isNewMax = m.hasValue.CompareAndSwap(false, true)
    69  		}
    70  		return // 0 as max: isNewMax true for first 0 writer
    71  	}
    72  
    73  	// check against present value
    74  	var current = m.value.Load()
    75  	if isNewMax = valueU64 > current; !isNewMax {
    76  		return // not a new max return: isNewMax false
    77  	}
    78  
    79  	// store the new max
    80  	for {
    81  
    82  		// try to write value to *max
    83  		if isNewMax = m.value.CompareAndSwap(current, valueU64); isNewMax {
    84  			if !hasValue0 {
    85  				// may be rarely written multiple times
    86  				// still faster than CompareAndSwap
    87  				m.hasValue.Store(true)
    88  			}
    89  			return // new max written return: isNewMax true
    90  		}
    91  		if current = m.value.Load(); current >= valueU64 {
    92  			return // no longer a need to write return: isNewMax false
    93  		}
    94  	}
    95  }
    96  
    97  // Max returns current max and value-present flag
    98  //   - hasValue true indicates that value reflects a Value invocation
    99  //   - hasValue false: value is zero-value
   100  //   - Thread-safe
   101  func (m *AtomicMax[T]) Max() (value T, hasValue bool) {
   102  	if hasValue = m.hasValue.Load(); !hasValue {
   103  		return
   104  	}
   105  	value = T(m.value.Load())
   106  	return
   107  }
   108  
   109  // Max1 returns current maximum whether zero-value or set by Value
   110  //   - threshold is ignored
   111  //   - Thread-safe
   112  func (m *AtomicMax[T]) Max1() (value T) { return T(m.value.Load()) }
   113  
   114  // tToUint64 converts T value to uint64
   115  //   - panic if T value is negative
   116  func (m *AtomicMax[T]) tToUint64(value T) (valueU64 uint64) {
   117  	var err error
   118  	if valueU64, err = ints.Unsigned[uint64](value, ""); err != nil {
   119  		panic(err) // value out of range, ie. negative
   120  	}
   121  	return
   122  }