Help coming up with reasons to not use `@isolated(any)`

I'm trying to come up with reasons why async function arguments that are sending or @Sendable would not be @isolated(any). Are there any disadvantages? Perhaps runtime overhead?

3 Likes

I'm sure you're aware of this, but for completeness, here's the guidance on when to use @isolated(any) from SE-0431:

When to use @isolated(any)

It is recommended that APIs which take functions that are likely to run concurrently and don't have a predetermined isolation take those functions as @isolated(any). This allows the API to make more intelligent scheduling decisions about the function.

Examples that should usually use @isolated(any) include:

  • functions that wrap the creation of a task
  • algorithms that call a function multiple times in parallel, such as a parallel map

Examples that should usually not use @isolated(any) include:

  • algorithms that preserve the current isolation, such as a non-parallel map; these functions should usually take a non-Sendable function instead
  • APIs that intend to call the function with a specific isolation, such as UI frameworks that expect their event handlers to be @MainActor or actor functions that run an operation on the actor
1 Like

Yes, but thank you for including this. I should have just done that originally.

I think the situations where it should not be used make total sense. But, as soon as you have a sending/@Sendable closure, it's more tricky! The annotation buys you flexibility. So it is tempting to just always include it, even if you cannot think of a reason today. And that got me thinking about the trade-offs involved.

2 Likes

I am having a hard time making up any advantages, as long as you are not involving task creation. Would you mind showing any examples? (Sorry, if I am missing anything obvious, just curious :grimacing:)

2 Likes

I would ask opposite question: what are the reasons for such functions to be marked with @isolated(any)? Unless there is an explicit need to carry isolation to the caller, I would hesitate to add it.

If I understand behavior correctly, for async function that passed in such way that is already isolated, it makes no sense[1] to additionally carry it on your own.


  1. any unnecessary switches are better to be eliminated within language runtime, and that might help to indicate one ↩︎

3 Likes

Ok fine :wink:

I think many users find this annotation to be confusing, myself included! Yet it shows up in some very common APIs. This forces users to think about it, even if it has no meaningful impact on callers.

I think an interesting simplification would be if all async closure arguments that are @Sendable or sending are implicitly @isolated(any). But I wanted to make sure I fully understood the implications.

From the current language behavior, this would introduce duality and confusing distinction between async and non-async closures, so while I don’t see any downsides to that in practical terms, I wouldn’t like this variety.

However, if we consider that default isolation inheritance for nonisolated functions will made to the language, then carrying isolation with async closures by default will make sense — arguably, not doing so will create difference in an opposite way in that case.

1 Like

Can you elaborate? I do not see how this is any more/less confusing than the implications of a sending/@Sendable async argument. What is it about retaining the static isolation that makes this different?

I’m not saying that @isolated(any) in either way (explicit or implicit) is confusing, only when we pair it with nonisolated async functions. The confusion for me comes in case we’ve got different behaviors for each part.

Currently neither nonisolated async functions nor plain async closures won’t keep isolation by default. In order to achieve this, you have to add additional code (attribute or isolated parameter) to enable functions keep this information. Which makes it simple to track and understand, as both behave in the same way. If async closures would make change to keep isolation by default, but nonisolated async functions won’t — we would have a distinct behavior which is hard to explain why it exists. That’s why I find this confusing if we just reason within current state of the language.

Now for the future Swift, where nonisolated async functions do inherit isolation by default, but async closures don’t — we end up in opposite situation where it’s hard to justify why closures don’t get this default behavior as well. That’s why it would be confusing not to make @isolated(any) default for async closures in the future Swift.

Hope it makes more clear what I meant by “confusing” :slight_smile:

1 Like

Perhaps it’s just me still learning, but I encountered some runtime crashes due to isolation assumptions within the SwiftUI module when using @Sendable closures that escaped outside of MainActor. I couldn’t pinpoint the exact issue with certainty—it was unsettling. So, I switched from @Sendable to @isolated(any), which resolved the problem. And now I'm also not sure when to use which but I'm sticking with @isolated(any) just in case until I gain a complete understanding of how it works.

I’m not sure whether this was a bug in Swift 6.0 or SwiftUI, but I hope it was a bug—because as far as I’m concerned, such issues should not compile, let alone cause runtime crashes.

1 Like

I think I should have included some more clear examples to help illustrate what I was getting at here.

func funcA(_ work: () async -> Void) {}

func funcB(_ work: sending () async -> Void) {}

func funcC(_ work: @Sendable () async -> Void) {}

I'm trying to figure out what downsides, if any, there are to annotating these closures with @isolated(any).

Is there any impact to the semantics from the caller's perspective?
How about from the callee?
Is there any additional runtime overhead?

I think the point about the potential change in isolation inheritance is super interesting, and I wish I wasn't being so slow, but I do not understand how that would affect the question.

Wow, I do not know how to reconcile the mechanisms I'm aware of that could cause unexpected executor-related crashes and the solution you used. I don't even see how it could work, given that @isolated(any) does not imply @Sendable.

I've be very curious to investigate this one further.

Huh? Somewhere in the proposals I came across that @isolated(any) implies @Sendable. That was recently, like few weeks ago but now I'm unable to look it up. Instead I found it doesn't anymore–for months? Now I'm confused even more.

1 Like

I don't think the semantics of @isolated(any) have ever deviated from what was in the current version of the proposal that introduced it.

And speaking of those semantics, the only I know of are:

  • The function must be called with await
  • The function variable now has a .isolation property

I don't think that this annotation has any visible impact on callers, and only potentially changes what consumers of the function variable can do. And in the case of an already async function, I don't think it changes anything at all.

That as well may be my missing of something in @isolated(any) understanding, making nonsense statements. In general I find the @isolated(any) has a (surprisingly) narrow scope of application in day-to-day code – only if I want to make some common and generalized function. But since that's also applies to isolated parameters, it makes me at least enough confident to look up for behaviour implications.

My idea that implicit @isolated(any) is for async closures – is what – default isolation inheritance is for non-isolated functions. Right now nothing carries an actor isolation in the by default, if that's subject of a change – then probably closures has to change as well and also carry isolation by default. At least, this seems logical to expect such parity...

1 Like

Honestly, I'm having a hard time figuring out if there are situations where this would be problematic.

But the easiest way to find out is to propose change! So that's what I did here:

1 Like