Am i using `AtomicReference` correctly?

this is yet another followup to Use swift-atomics to track a “largest seen value”?

i now have AtomicTime that looks like:

import Atomics

final
class AtomicTime:Sendable, AtomicReference
{
    let value:ClusterTime.Sample

    init(_ value:ClusterTime.Sample)
    {
        self.value = value
    }
}
extension AtomicTime
{
    convenience
    init?(_ time:ClusterTime)
    {
        ...
    }
}
@frozen public
struct ClusterTime:Sendable
{
    public
    let max:Sample?

    init(_ sample:Sample?)
    {
        self.max = sample
    }
}
extension ClusterTime
{
    func combined(with sample:Sample) -> Self
    {
        guard let max:Sample = self.max
        else
        {
            return .init(sample)
        }
        if  max.instant < sample.instant
        {
            return .init(sample)
        }
        else
        {
            return self
        }
    }
    mutating
    func combine(with sample:Sample)
    {
        self = self.combined(with: sample)
    }
}

which is part of an actor that looks like:

public final
actor Cluster
{
    private nonisolated
    let atomic:
    (
        ...
        time:UnsafeAtomic<AtomicTime?>
    )
}

the modeled cluster time can be read without blocking with:

extension Cluster
{
    /// The current largest-seen cluster time, if any.
    public nonisolated
    var time:ClusterTime
    {
        .init(self.atomic.time.load(ordering: .relaxed)?.value)
    }
}

and it can be updated (in isolation) with:

extension Cluster
{
    func push(time:ClusterTime.Sample)
    {
        self.atomic.time.store(.init(self.time.combined(with: time)),
            ordering: .relaxed)
    }
}

i read the old value and store the new value with relaxed memory ordering, since it doesn’t need to synchronize with any other atomics. is this correct?

I don't expect the use of .relaxed itself would by itself break anything here -- relaxed loads/stores of atomic references still end up calling the usual Swift retain/release entry points, so you are still guaranteed to get the same fences as you normally get when loading/storing a Swift reference. (Which isn't a very strong guarantee, but it at least gives you the usual semantics.)

In addition, atomic strong references currently ignore the specified memory ordering anyway, and unconditionally use acquiring/releasing ordering for their own extra atomic operations. (Implement sequentially consistent memory ordering for strong references · Issue #3 · apple/swift-atomics · GitHub) This isn't ideal, but it ought to be okay unless you end up needing sequential consistency.

By the way, I'd be very much interested in hearing about practical experience using AtomicReference -- I think it's a reasonably simple memory reclamation solution, but I don't really know how well it works in real life situations. For the simplest cases, it may or may not compare favorably against a regular (fast) lock -- it may be worth running a few benchmarks and see. (For more complex situations, its performance is unlikely to compare well against custom-tailored memory reclamation strategies, but it seems hard to beat its semantics -- it sort of emulates the ease of memory reclamation in a garbage collected platform, so using it can be a nice way to port algorithms that were targeting one of those.) Do let me know if it proves useful (or not!).

2 Likes

to me, the usefulness of AtomicReference was never in doubt: of course it’s useful! the alternative is submitting a read operation to an actor, and waiting for that future to complete. so the fact that it is the only straightforward way to publish data that is larger than 8 bytes long without having to figure out how to implement a lock (something i thought was a bad idea, according to the other thread) or implement a custom memory allocator is enough to make it useful.

of course there is one reason why AtomicReference is not useful: it’s really hard to compile code that uses it, because of all the unsafe flags that need to be passed to make it available in the first place. i got pretty far by passing them manually from the command line, but this is not really well-supported by SPM, and they don’t seem to work with cSettings/swiftSettings in the package manifest. and packages that use unsafe flags cannot themselves be dependencies of other packages.

so it would be really helpful to me if this was enabled by default and i could depend on swift-atomics with AtomicReference as a normal package.

1 Like

The real question isn't that -- what I'd love to get more information on is how AtomicReference relates to a regular strong reference that is protected by a boring old lock, such as an os_unfair_lock; particularly when most accesses are reads.

(I do suspect that AtomicReference would come out the winner in some situations at least, but it's unlikely I'll have time to implement proper performance tests anytime soon -- so while it's encouraging that the construct seems helpful, I'm very much looking for real life results that compare it against locking.)

1 Like