@concurrent and main actor isolation

With SE-0461 we gained the ability for inheriting actor isolation from the caller through nonisolated(nonsending) functions. We also gained @concurrent to offload work and to not inherit the caller's actor.

The proposal states the following:

Only (implicitly or explicitly) nonisolated functions can be marked with @concurrent ; it is an error to use the these attributes with an isolation other than nonisolated , including global actors, isolated parameters, and @isolated(any) :

However, the following code does compile with the Swift 6.2 toolchain:

@MainActor
class Model {
    // this is allowed
    @concurrent func testing() async {
        
    }
}

When I explicitly isolate testing() with @MainActor, I do get a compiler error.

@MainActor
class Model {
    // this is not allowed
    @MainActor @concurrent func testing() async {
        
    }
}

Similarly, the following compiles when default isolation is set to the main actor:

class Model {
    // this is allowed
    @concurrent func testing() async {
        
    }
}

I would expect that @concurrent can't be applied to any of these functions because they're isolated to the main actor, and @concurrent can only be applied to nonisolated functions.

Am I correctly assuming that this is a bug or is this some special behavior for functions that are declared on main actor annotated types?

1 Like

You can give any type with isolation functions outside of that isolation domain. So an actor can have members (variables or functions) that are not isolated or isolated differently. That's not a bug.

actor MyUser {
  nonisolated func sayName() {
    // Does not get isolated by `MyUser` and would need to `await` any variables just like outside the actor
  }
}

The same is true for global actors like MainActor too

Yes, that's not what I'm asking though. I'm specifically asking about @concurrent which cannot be applied to isolated declarations:

// this is not allowed
@concurrent @MainActor func test() async {}

However, if I have a main actor annotated type I can annotate functions defined on that type with @concurrent

@MainActor class Model {
  @concurrent func test() async {}
}

I wouldn't expect this to be allowed because test is main actor isolated. Yet the compiler allows me to mark the function as @concurrent.

The proposal says "Only (implicitly or explicitly) nonisolated functions can be marked with @concurrent"

I would think that test in this case is neither implicitly nor explicitly nonisolated since it's a member of a MainActor isolated type.

Hmm, I've not read the full document in that much detail. But I would expect @concurrent to apply nonisolated implicitly.

1 Like

That might actually be the case :thinking:

actor MyActor {
    @concurrent func testing() async {
        
    }
}

This also compiles and testing is very much NOT nonisolated here.

So I guess this line from the proposal "Only (implicitly or explicitly) nonisolated functions can be marked with @concurrent" isn't fully correct in the sense the as far as I've seen the proposal doesn't mention that @concurrent will implicitly nonisolate a function...

3 Likes

The language you cite is also in the proposed documentation for data-race safety discussed in [1]. I take that documentation as authoritative and cumulative, so your feedback is relevant to that active discussion and doc PR and would likely be welcome there.

The use-case of running an actor method off the actor seems essential. I wonder if inference should/does permit @concurrent as a more-explicit override for classes and actors. However, it should probably balk at overriding conflicting declarations of protocol requirements or method overrides. Overriding those might break (the Liskov substitution principle for) subtype delegates.

So perhaps instead of

Only non-isolated functions can be @concurrent functions, and marking a function with @concurrent implies nonisolated

Is this closer to the intended reality?:

Marking a function with @concurrent implies nonisolated. This may override the inference from otherwise unmarked actor and class functions, but not when overriding protocol requirements with actor isolation or when overriding class functions with explicit actor isolation.

[1] https://forums.swift.org/t/rfc-data-race-safety-chapter-in-the-tspl-language-reference/80381

Chiming in on this, as this also confused me a lot during WWDC sessions.
Am I right that marking a type or function as nonisolated just means that it can, but not has to run on any isolation domain within Swift 6.2? If I am not mistaken, marking a function as nonisolated in versions before Swift 6.2 meant that it will definitely run on the concurrent cooperative pool.
And even further, does marking a function as @concurrent now tells the compiler that this function will for sure run on the concurrent pool now? (Which basically is what nonisolated did before, if all my assumptions are right).
Would be awesome if someone could jump in here and clarify if those assumptions are right and if not, how it works otherwise!

1 Like

Yes, it sounds like you're correctly understanding the new semantics of being nonisolated and @concurrent

I wrote about the changes here just in case: Exploring concurrency changes in Swift 6.2 – Donny Wals

1 Like

Thanks for the answer.
In your post you are writing that, within the Swift 6.1 mode, loadUserPhotos is nonisolated and async, however the function is just marked as async. Did that mean that nonisolated was basically the default if there was no other isolation (like e.g. MainActor) set?
And with Swift 6.2 we basically have to opt into this, as its otherwise MainActor by default, leaving custom actors aside.

And another thing within Swift 6.2: If I just mark a function to be nonisolated (and not async as well), it will just run on the callers thread, right?

Did that mean that nonisolated was basically the default if there was no other isolation (like e.g. MainActor) set?

Yes

And with Swift 6.2 we basically have to opt into this, as its otherwise MainActor by default, leaving custom actors aside.

Yes, when you've enabled default actor isolation then you'd be isolated to the main actor unless you've specified different (by marking as nonisolated or @concurrent explicitly)

If I just mark a function to be nonisolated (and not async as well), it will just run on the callers thread, right?

Yes, this behavior should be the same for async and non-async functions in Swift 6.2

1 Like

Thanks, this helps a lot understanding those changes. Really seems that the steps taken with approachable concurrency simplify things in the future understanding the mental model behind it.

1 Like

Maybe this deserves more explicit documentation. I believe the type checker infers nonisolated for @concurrent here.

disclaimer: I don't work on the compiler

3 Likes