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 }