Is this an ok solution to achieve shared instance of actor using async init?

I compile an SPM target with:

.unsafeFlags([
    "-Xfrontend", "-warn-concurrency",
    "-Xfrontend", "-enable-actor-data-race-checks",
]),

Conceptually, I wanna do this, but can't, due to async' call cannot occur in a global variable . So I need to find some alternative:

public final actor FooStore {

    public static let shared: FooStore = await FooStore() // ❌ 'async' call cannot occur in a global variable initializer

    private let foo: Foo
    private init() async {
        self.foo = await Self.initFooUsingMustBeAsyncMethod()
    }
}

So instead I naively try this:

public final actor FooStore {
    private static var _shared: FooStore?

    /// Completely fine that this is a function
    public static func shared() async -> FooStore {
        // ⚠️ WARNING: Reference to static property '_shared' is not concurrency-safe because it involves shared mutable state
        if let _shared {
            return _shared
        }
        let shared = await FooStore()
        
        // ⚠️ WARNING: Reference to static property '_shared' is not concurrency-safe because it involves shared mutable state
        _shared = shared
        return shared
    }

    private let foo: Foo
    private init() async {
        self.foo = await Self.initFooUsingMustBeAsyncMethod()
    }
}

OK I understand the warning and take it seriously, bad idea!

So I figured I'd try fixing those warnings, by... wrapping an actor around it..?! And hey, it compiles without warning!

public final actor FooStore {
    
    private final actor StoreOfFooStore {
        private var _shared: FooStore?
        fileprivate func shared() async -> FooStore {
            if let shared = _shared {
                return shared
            }
            let shared = await FooStore()
            _shared = shared
            return shared
        }
    }
    private static let storeOfSharedFooStore = StoreOfFooStore()

    /// Completely fine that this is a function
    public static func shared() async -> FooStore {
        await Self.storeOfSharedFooStore.shared()
    }
    
    private let foo: Foo
    private init() async {
        self.foo = await Self.initFooUsingMustBeAsyncMethod()
    }
}

Is this an OK solution, as in thread safe, data race safe?

I think you have a potential reentrancy problem in StoreOfFooStore.shared(). Since let shared = await FooStore() may suspend, another task could call it again before the _shared property becomes non-nil. This means you'd run FooStore.init multiple times.

1 Like

Thank you @ole ! Howcome Swift compiler did not find flag this as a warning? Did I manage to fool it (and myself...) by use of the nested actor (too complex for static analysis?)

And I guess this version has the same problem, since it also contains the line: let shared = await FooStore()

public final actor FooStore {

	/// ActorIsolated by PF: https://github.com/pointfreeco/swift-dependencies/blob/main/Sources/Dependencies/ConcurrencySupport/ActorIsolated.swift#L42
	private static let _shared = ActorIsolated<FooStore?>(nil)

	public static var shared: FooStore {
		get async {
			if let shared = await _shared.value {
				return shared
			}
			let shared = await FooStore()
			await _shared.setValue(shared)
			return shared
		}
	}

	private let foo: Foo
	init() async {
        self.foo = await Self.initFooUsingMustBeAsyncMethod()
	}
}

Is it (actor with static shared using async init of Foo?) at all achievable?

I guess I would be open to some kind of solution using DispatchSemaphore or NSRecursiveLock or something? Or does anyone have a better idea?

Can I used ManagedCriticialState from AsyncAlgorhtms perhaps?

Just to touch on this, current actors in swift are reentrant so for the language this code is totally valid. It’s up to the programmer to maintain invariants between suspension points, something to be aware. so nothing to do with the code you have going here ^^

1 Like

As @Alejandro_Martinez said, Swift Concurrency won't protect you from "logic races", i.e. code that is totally safe, but doesn't do the right thing. Concurrency is still hard! You have to take into account that the entire non-local state of your program may have changed after a suspension point.

1 Like

Sure. But I’m still asking for help/guidance on how to achieve what I want.

Which is an (safe/correct) actor that has a shared instances, which needs the be initialized asynchronously.

Surely I cannot be the only person in the world wanting to achive this use case?