Is there any "real" difference between `@MainActor @Sendable () -> ()` and `@MainActor () -> ()`

Per the documentation of Sendable, classes isolated to the Main Actor are implicitly Sendable. Closures are basically just a shorthand for a class with one function and only private state.

There's also this pitch which says it will "Infer the @Sendable attribute for global-actor-isolated functions and closures."

My intuition (which comes mainly from threads/GCD) says that if the compiler guarantees that a particular piece of code is always run on the main thread (or the equivalent in "isolation domains" or "global actors") then it doesn't matter how many threads have a reference to the function, because they always await it running on main.

So, should I always mark closures that are isolated to a global actor as @Sendable, in preparation for the day that the Swift compiler infers this for me? Or is there ever a situation where a globally isolated closure isn't sendable, or some quirk of the compiler that makes this a bad idea today?

2 Likes

Hi,

Under the current concurrency rules, a non-Sendable globally-isolated closure is not really useful, as you can only use it if the current context is already isolated to the global actor (naturally, this won't require sendablity across isolation domains). Since the context is already isolated to the main actor, and the closure is non-Sendable, you don't even need to mark the closure main-actor isolated because it will be called from it anyway.

However, until SE-0434 lands, I think that from the usability perspective, you should mark globally-isolated closures as Sendable because then you'll be able to use them in, for example, a Task{} closure.

Your intuition is mostly correct, if the closure is isolated to the main actor, this means that it will never be called concurrently since all calls to it will be queued on the main actor (this is a very very abstract high-level intuition). So therefore, it is safe to infer @Sendable for usability of such a closure.

I hope this helps!

-- Sima

4 Likes

You could mark a closure @MainActor in an enum's associated value. This will enable you to pass around the closure to non-main actor, isolated contexts, and result in the compiler ensuring that when you're ready to call it, it will be on the main actor.

Seems like compiler sees no difference between those two since swift 6.

I reported a bug for inconsistent behavior: @MainActor closure type description adds '@Sendable' to the description. · Issue #75683 · swiftlang/swift · GitHub

That's not a bug, this's an intended behaviour introduced by SE-0434, because isolated on global actor closure is implicitly @Sendable, the same way isolated on global actor type is.

1 Like

But if I remember correctly, in debugger it does not show me "@Sendable" when I do a mirror reflection of a non-sendable closure that is @MainActor

Sendable is a “marker” protocol and has no runtime presence. The same is likely true of the @Sendable annotation

1 Like

Seems like @Sendable has runtime presence.

Basically I can not cast a runtime object to @MainActor ()->() because compiler prevents me by converting it to @MainActor @Sendable ()->() and casting fails. That is an issue.

How its not a bug, it prevents me to do casting of a runtime object that is @MainActor ()->() and not @MainActor @Sendable ()->()? Then there is a bug in casting?

See my image I posted above.

But afer reading your link, and if i got that correct, its a feature not a bug!?

General inference of global-actor isolated closure as @Sendable is a feature, I can't help for the casting part as I have never used casting of a closures. It might be a bug, but that's better to clarify with proposal authors as they might know better internals of implementation (I'm not even sure how such markers should behave at runtime).

1 Like