How to use swift-atomics to expose non-isolated state of an actor?

i have an actor that maintains a value, let’s call it currentTime:UInt64, and i would like for other code outside of the actor to be able to read it without awaiting on the actor lock.

actor SomeActor
{
    // nonisolated
    var currentTime:UInt64
}

it is okay to read the currentTime even if the actor is in the middle of an operation, because the actor should only update it atomically.

i am looking at the UnsafeAtomic<Value> type from swift-atomics package, but i am really struggling to find structured concurrency examples for how to use this type correctly with a swift 5.5+ actor.

can anyone point me towards some docs for how to get started with this package? the README talks a lot about what the package supports, which i suppose is useful to people who already know a lot about UnsafeAtomic, but it doesn’t seem to have any guides for how to use the contents of the package.

Did you try:

nonisolated var currentTime: ManagedAtomic<UInt64>

alas, you cannot use nonisolated on a var, though ManagedAtomic works just as well with a let.

(why doesn’t ManagedAtomic conform to Sendable though?)

but i am looking at UnsafeAtomic because an actor already has a managed allocation, which means accessing a ManagedAtomic would defererence three (?) pointers (SomeActorManagedAtomicUnsafeAtomic)…

so far i am using UnsafeAtomic.destroy in the actor deinit, but i am unsure if this is correct.

I don't think you can do this safely without Exposing the Memory Locations of Class Instance Variables.

ManagedAtomic uses _getUnsafePointerToStoredProperties (swift-atomics/HighLevelTypes.swift.gyb at 919eb1d83e02121cdb434c7bfc1f0c66ef17febe · apple/swift-atomics · GitHub), which you could adjust for the size of the Actor header if you really wanted to (adapt swift/Builtin.swift at 486dc27e1d6736b2b0a7b93f3ffba9bae52d99e8 · apple/swift · GitHub; you can use Unmanaged.passUnretained.toOpaque() in place of Builtin.bridgeToRawPointer(x)). Just using a let currentTime: ManagedAtomic<UInt64> and accepting the pointer indirection is by far going to be the easiest and safest route, though – it's only one additional indirection/two pointers in total.

3 Likes

after reading the source for the atomics i think i misunderstood the difference between ManagedAtomic and UnsafeAtomic, they are both conceptually pointers to remote storage but ManagedAtomic opts you into reference counting lifecycle management. but ARC on the atomic allocation is not needed if you already have a reference-counted actor managing it.

so i suppose i cannot easily have an atomic stored inline within an actor’s allocation but i can opt it out of ARC by using UnsafeAtomic.

hopefully my understanding is correct (?)

That matches my understanding. UnsafeAtomic<T> is effectively an UnsafeMutablePointer<T.AtomicRepresentation> with atomic methods exposed on the type, so you manage it as you would an UnsafeMutablePointer.

1 Like