Swift atomics are rather awkward

i find that swift atomics (as provided by the swift-atomics package) are rather awkward to work with.

UnsafeAtomic lifetime bounds

ManagedAtomic adds a lot of overhead to something that often already has its lifetime bound to some class/actor instance. so i often prefer UnsafeAtomic over ManagedAtomic. but as the name would suggest, it is very awkward to expose a property of type UnsafeAtomic.

{
    private nonisolated 
    let _latency:UnsafeAtomic<Nanoseconds>

    public nonisolated 
    var latency:UnsafeAtomic<Nanoseconds>
    {
        _read { yield self._latency }
    }
}

it’s also never been clear to me if this is even sufficient to inhibit certain compiler optimizations that might cause a load from a deinitialized atomic.

Optional AtomicValues

a lot of stateful things are a good fit for Optional<T>. but there is not a natural way to model an optional value atomically.

{
    private nonisolated 
    let _lastQueryDuration:UnsafeAtomic<Nanoseconds?>
}

one workaround is to use AtomicReference which supports optionals. but oftentimes the value is small enough to fit easily in a doubleword, and forcing an allocation for every state update just seems silly.

Atomic arrays

much of what has been said about Optional atomics also applies to Arrays of atomics. meaning: publishing some immutable array of values.

{
    private nonisolated 
    let _serverHealth:UnsafeAtomic<[(host:String, ping:Nanoseconds)]>
}

again, AtomicReference is a possible workaround. but this seems unnecessary. an array is just a pointer to some reference-counted storage. we ought to be able to publish an immutable array of values atomically without wrapping it in another class allocation.

4 Likes

UnsafeAtomic and ManagedAtomic are shipping in the swift-atomics package, which is outside the Swift Evolution process. It is probably best to discuss their use on the relevant forum, not this general language evolution forum.

For reference, SE-0410 introduced struct Atomic to the Swift Standard Library. The proposal includes a way to make optionals of custom types be atomic-able -- it's done by conforming the type to the AtomicOptionalRepresentable protocol.

"Atomic arrays" are not a thing. I strongly recommend against trying to make Atomic<Array<Foo>> work -- the effort is (1) very, very unlikely to be successful, and (2) conforming stdlib types to stdlib protocols is a privilege reserved for the stdlib.

Instead, I highly recommend serializing access to a regular Array with a lock.

If you have a huge amount of free time that you're willing to spend on reading relevant research papers and then debugging race conditions, then there is also the option to build a standalone concurrent array type, carefully tailored to fit your specific use case at hand. (A "concurrent array type" can mean any one of a multitude of different constructs.) struct Atomic is an incredibly powerful tool that enables building such types in Swift; however, doing so is of roughly similar complexity (or worse) as building these things using std::atomic in C++ -- which is to say, it is a mind-blowingly frustrating proposition.

7 Likes

no, this is not really what i am trying to do; i am not mutating anything concurrently. i tried to use example variable names that would evoke this, but i guess that got lost in the nuance.

the pattern here is write once and publish for the whole world to see. here’s a simple example:

actor ClusterMonitor
{
    private 
    var serverPingsByHost:[(host:String, ping:Nanoseconds)]

    nonisolated 
    let serverHealth:UnsafeAtomic<[(host:String, ping:Nanoseconds)]>
}

the property serverPingsByHost is the ground truth for the server pings, and access to it is serialized by the actor, like the “lock” you mentioned. components that rely on always reading the most up-to-date ping values await on the actor.

could serverPingsByHost turn into a concurrent array (dictionary?) that would be safe to update without awaiting? with enough effort, sure!

but why bother. instead of reading a research paper, you could just write some (isolated) code on the actor.

on the other hand, serverHealth and its users are different. things that read serverHealth do not care about reading the freshest, latest serverPingsByHost. its users (like a UI render loop) only need to see a recent copy of serverPingsByHost that will not lag unreasonably behind the ground truth. the goal here is to run code (like a render loop) that can read this property without actually running on the actor. because if it is running on the actor, then that means the actual important work the actor needs to be doing is not happening.

1 Like

Updating a refcounted pointer atomically is a nontrivial operation and can't be done by a plain UnsafeAtomic alone, so you would need to either use AtomicReference or else have the atomic variable be Unmanaged and manage the lifetime of the referenced array yourself (which is also a pretty hard problem AIUI). The good news in the hopefully near term is that a noncopyable variant of AtomicReference should hopefully be implementable without adding another layer of indirection to the reference. However, it's also very situational whether the lock-free shenanigans of AtomicReference are even a win over taking a lock; if you don't expect much contention, taking an uncontended os_unfair_lock, reading the value, and releasing the lock is pretty hard to beat.

6 Likes

Right, os_unfair_lock, Linux futex based locks, and even Windows's SRWLOCK are all really really cheap to acquire in the uncontended case and unlocking is basically free in that scenario as well (as opposed to waiting for the system to fully unlock your lock). Even in the somewhat contended case, waiting in line to perform X amount of work with whatever state the lock is protecting, is sometimes more favorable than performing X amount of work, attempting to write your results, and then having to repeat doing X amount of work and write your results until you win the exchange.

1 Like

all the updates to the internal state are computed on the actor. all the updates to the shared, public state are also written by the actor - by copying (retaining) the internal array to the atomic pointer.

locks to me are a low-level, unsafe C construct to be avoided whenever possible. atomics are also low-level and easy to screw up, but they are less perilous than locks, because you don’t really care about ordering, you only care about propogation of some value whose ground truth is external to the atomic.

you can already do this safely with ManagedBuffer in place of Array. the issue is ManagedBuffer is just a gnarly convoluted thing to use when all you want is to store some inline buffer of immutable values.

1 Like

I think you got these reversed. Atomics are the very low-level, unsafe construct that are hard to use and best avoided unless you have a really specialized use case. Locks are relatively well-behaved and easy to understand by comparison. In the sort of case you're talking about, where you're only copying the value opportunistically to the public interface, then you only need to hold the lock for a well-defined amount of time while you read or write the atomic reference, so there's no real risk of leaving the lock dangling or deadlocking.

8 Likes

im not sure i agree with this. when i make mistakes with atomics, there is usually some kind of circularity or feedback going on, where the value stored in the atomic influences the later values that get written to that atomic. but using an atomic as a one-way conduit for some changing data for display purposes is not particularly hazardous.

i don’t get the impression that locks are as simple as you suggest. the type of lock needed here is likely some sort of reader-writer lock that permits concurrent readers. based on this thread the advice is to “write your own”, which i’m not enthusiastic about.

2 Likes

Even reader-writer locks generally aren't worth the added overhead. For something like what you're talking about, either a lightweight lock like os_unfair_lock or AtomicReference would be appropriate depending on the level of contention. All you're doing is a single read/write inside the critical section so I don't think it needs to be "async friendly" since you probably aren't blocking for a significant amount of time.

3 Likes

I fully agree with what @Joe_Groff suggests here. Locks are the easier to use and safer primitive. For what it’s worth, almost all code in NIO and higher level libraries uses locks to protect its state even code such as our various EventLoop implementations are all implemented using locks. I would recommend first starting with a lock and only look at atomics if your performance measurements indicate high contention on the lock.

2 Likes

I also agree, but can just reflect that it’d be nice if we’d get locks and semaphores in some form in the standard library, but now we get atomics :slight_smile:

I understand (and agree with and like) the approach of swift concurrency, but looking forward to when we have first class features for such constructs too….

8 Likes

right, lock implementations in swift are kind of a jungle if you’re not using Foundation.

AtomicReference is the right way to go. allocating the object is expensive, but for something that is updated in second intervals that’s not really a concern.

AtomicReference doesn’t work with Array because Array is a struct wrapper around a pointer, and AtomicReference doesn’t know how to refcount a wrapped pointer. the low-effort, low-reward answer is to wrap the array in a class. the high-effort, marginally higher-reward answer is to roll your own immutable ManagedBuffer-backed immutable array type that contains the elements inline. but i wish it were not so difficult to do the latter.

2 Likes

That Array is still a less-than-maximum-atomic-size struct containing a single refcounted field means it should theoretically be possible to implement AtomicReference containing it. I'm not sure how its requirement would be represented in the language, but maybe we could provide an AtomicReferenceRepresentable protocol, like we decided to do for the standard library atomics, so that Array, Dictionary, Set, and similar types can also be used with it.

1 Like

And half of my wish seems to come true much faster than I could have hoped, super!

1 Like