Would that work and if not why? Pseudocode:
func set(parameters: ...) {
while true {
let a = loadA()
let b = loadB()
let (needToChange, newA, newB) = calculateValues(a, b, parameters)
if !needToChange { break }
let (changedA, origA) = compareExchange(expected: a, desired: newA)
let (changedB, origB) = compareExchange(expected: b, desired: newB)
if changedA && changedB {
break
}
}
}
Edit. Generalised real code, untested
import Atomics
func changeValuesAtomically<T>(_ atomicValues: [ManagedAtomic<T>], calculateValues: ([T]) -> [T]?) {
while true {
let curValues = atomicValues.map {
$0.load(ordering: AtomicLoadOrdering.relaxed) // order?
}
guard let newValues = calculateValues(curValues) else {
break
}
precondition(newValues.count == atomicValues.count)
var done = true
for i in 0 ..< atomicValues.count { // three-way zip ?
let atomic = atomicValues[I]
let curValue = curValues[I]
let newValue = newValues[I]
let (changed, oldValue) = atomic.compareExchange(expected: curValue, desired: newValue, ordering: .acquiringAndReleasing) // order?
// any good use for oldValue here?
if !changed {
done = false
break
}
}
if done { break }
}
}
// Usage:
var timeStamp = ManagedAtomic(0)
var somethingElse = ManagedAtomic(0)
var newTimeStamp = 123
var newSomethingElse = 456
changeValuesAtomically([timeStamp, somethingElse]) { values in
precondition(values.count == 2)
if newTimeStamp <= values[0] {
return nil
}
return [newTimeStamp, newSomethingElse]
}
However, for time critical use I'd not use dynamically allocated arrays. De-generalised version:
import Atomics
func changeValuesAtomically<T1, T2>(_ atomic0: ManagedAtomic<T1>, _ atomic1: ManagedAtomic<T2>, calculateValues: (T1, T2) -> (T1, T2)?) {
while true {
let curValue0 = atomic0.load(ordering: .relaxed) // order?
let curValue1 = atomic1.load(ordering: .relaxed) // order?
guard let (newValue0, newValue1) = calculateValues(curValue0, curValue1) else {
break
}
let (changed0, oldValue0) = atomic0.compareExchange(expected: curValue0, desired: newValue0, ordering: .acquiringAndReleasing) // order?
// any good use for oldValue here?
if !changed0 { continue }
let (changed1, oldValue1) = atomic1.compareExchange(expected: curValue1, desired: newValue1, ordering: .acquiringAndReleasing) // order?
// any good use for oldValue here?
if !changed1 { continue }
break
}
}
// Usage:
var timeStamp = ManagedAtomic(0)
var somethingElse = ManagedAtomic(0)
var newTimeStamp = 123
var newSomethingElse = 456
changeValuesAtomically(timeStamp, somethingElse) { timeStamp, somethingElse in
if newTimeStamp <= timeStamp { return nil }
return (newTimeStamp, newSomethingElse)
}
Note on atomicity of loads. This sequence of loads:
let curValue0 = atomic0.load(ordering: .relaxed)
let curValue1 = atomic1.load(ordering: .relaxed)
// checkValues here, e.g. precondition((curValue1 + curValue0) % 3 == 0)
doesn't give "overall atomic result", for example if you were to maintain the invariant that "somethingElse + timeStamp must be divisible by 3"
that invariant could be broken at "checkValues here" point above. If that kind atomicity is required – one (perhaps non ideal) way would be to loop those two lines until the wanted invariant is satisfied, or in the code above just "continue":
while true {
let curValue0 = atomic0.load(ordering: .relaxed)
let curValue1 = atomic1.load(ordering: .relaxed)
if !invariantSatisfied(curValue0, curValue1) { continue }
....
}