Setting properties in nonisolated init for MainActor-isolated class

Is there a way to make a nonisolated init which sets a property that should, after initialization, only be set/accessed from the MainActor?

I've got a legacy object that I'm trying to make concurrency safe, but I'm struggling a bit.

Here's a code sample to make the question clearer:

@MainActor
class Foo {
    nonisolated static let shared = Foo() 
    
    var mybar: Bar // I only need to get this from the main actor
    // In the real code there are many properties; that's why I'm not tagging
    // each one @MainActor on its own, but it should boil down to the same thing.
    
    nonisolated init() {
        mybar = getCachedBar() // Warning: Main actor-isolated property 'mybar' can not be mutated from a nonisolated context; this is an error in the Swift 6 language mode
    }
    
    nonisolated func updateBar() {
        someDispatchQueue.async {            
            let newBar = createNewBarOMGItTakesSoLong()
            DispatchQueue.main.async {
                self.mybar = newBar
            }
        }
    }
}

I have a singleton Foo.shared which manages a Bar. I only need to really use this on the main thread. But when I update the bar it takes a good long while & I'd like to do that on a different thread. I also sometimes find out that I'd like to update the bar from a background thread. To me it makes most sense to have Foo be @MainActor but Foo.shared and updateBar be nonisolated.

To have Foo.shared be nonisolated, obviously the init needs to be nonisolated as well. So far so good.

But I can't set the mybar property inside a nonisolated init without a concurrency warning. Is there a way I can convince the compiler that it's OK to mutate on any thread at init time, but after that we should only allow MainActor?

Failing that, is there a way I can keep my same general design but tweak it to satisfy the compiler? Or am I cursed to needlessly call Foo.shared.updateBar() on the main actor?


I'm using Swift 6.2.3 from Xcode 26.2 if that helps!

If Bar is Sendable, this should work.

If Bar is not Sendable, you'll need to use sending to prove this is safe. Try ensuring that getCachedBar() -> sending Bar and createNewBarOMGItTakesSoLong() -> sending Bar.

1 Like

Yeah Bar isn't Sendable. Marking these functions sending makes a lot of sense. Unfortunately though that doesn't fix the warning.

On the init line setting mybar:

warning: main actor-isolated property 'mybar' can not be mutated from a nonisolated context; this is an error in the Swift 6 language mode

I've created an actually compilable version of my example that has definitions for all the functions/types so it's a little more clear. @KeithBauerANZ does this gist look like what you meant?

I think this is a compiler bug and/or unintentional language limitation, but I'd like to hear from a language team member before I go embarrassing myself with a bug report :sweat_smile:, maybe @hborla or @Michael_Gottesman

The obvious solution for now is just to wrap mybar up in a Mutex, but if you do that you'll quickly run afoul of Can't send into Mutex · Issue #81546 · swiftlang/swift · GitHub when you try to update the value.

If you're on an Apple platform, you could use the init(uncheckedState:) and withLockUnchecked methods of OSAllocatedUnfairLock

1 Like

Thanks!

Went ahead and filed a bug so it doesn't get lost in the forums: Actor-isolated properties cannot be assigned in non-isolated inits · Issue #87690 · swiftlang/swift · GitHub

2 Likes

If I understand it correctly, for a global function that (a) neither is isolated to any actor, (b) nor has any parameters, the return value of it should already be in a disconnected region. So I think changing the function into -> sending Bar would have zero effect.

From another perspective, the proposal SE-0327 might contain a clue.

In that proposal, it first states that when dealing with actors (not-GAITs), if the initializer has a "nonisolated self", then inside it we are OK to initialize member variables with NonSendables, until self is passed out for the first time. The following code compiles as expected:

actor A {
    var mybar: Bar 

    init() {
        mybar = Bar()  // OK!
    }
}

class Bar {
    var i = 0
} // Nonsendable

And later in the proposal, it talks about GAITs:

A non-isolated initializer of a global-actor isolated type (GAIT) is in the same situation as a non-async actor initializer, in that it must bootstrap the instance without the executor's protection. Thus, we can construct a data-race just like before

However this code does not compile in reality:

@MainActor
struct B {
    var mybar: Bar 

    nonisolated init() {
        mybar = Bar()  // error just like OP
    }
}

class Bar {
    var i = 0
} // Nonsendable

I've checked multiple versions of the compiler and I believe the relevant part about GAITs was originally implemented in Swift 5.10, but since Swift 6.0 there's a regression.

I won't profess to be an expert on this, but I think in my very original post adding the sending makes sense - the compiler doesn't know that I'm not sharing the returned Bar with something else, no?


This is very interesting! My read of the proposal is that it should, indeed, work for my @MainActor init. Thank you for checking all these versions - I'll add this info to the bug report.

Just an FYI. This is actually something that I am actively working on already.

3 Likes

Wonderful news!