[Pitch] Inherit isolation by default for async functions

No, because this requires adding a parameter to a function, which is an ABI change. It's critical for async functions to be able to preserve their current semantics without breaking ABI.

This does not solve the problem of nonisolated having different semantics on synchronous versus async functions. @isolated(any) also doesn't have any impact on the inferred isolation of a closure. It only means that you can recover the captured isolation as an (any Actor)? value.

This proposal is indeed a semantic change to the concurrency model, and I believe it's a critical enough problem to make such a change. However, every semantic change inflicts additional work on programmers, and we should not change more than we believe is truly necessary. There might be a better keyword name than nonisolated that we could have used, but I don't have any evidence that nonisolated on synchronous functions has been that difficult for people to understand that it means it can be called from anywhere, and it runs wherever its called. I've definitely seen common misconceptions that nonisolated is not safe, but in fairness, most people have been using minimal concurrency checking where there are many ways to violate data-race safety without compiler warnings. nonisolated is also used on properties to mean that the property is safe to access from any isolation domain. I'm pretty strongly against any direction that renames or removes nonisolated, or any of the other existing isolation annotations, because that means invalidating a lot of existing code that isn't actively problematic.

@Jumhyn just noted this, but the idea to use a variation of the async effect to specify isolation or the fact that a function runs on the global executor cuts off the future direction of applying this annotation to synchronous functions:

Yes.

I agree with this; there are definitely cases where switching to the generic executor helps performance, but there are also cases where it hurts performance due to unnecessary executor switching.

I'm also not convinced that the current default is helping people improve performance by offloading work to the generic executor other than by providing a convenient way to express it after discovering you explicitly need to move expensive computation off the main actor, e.g. through profiling your code.

Finally, I believe the current default optimizes for the advanced programmer who understands parallelism, which hurts progressive disclosure of the concurrency model. The philosophy I think we should follow is that the language should not impose parallelism on programmers by default; that should be a deliberate decision that is explicit in source (while still being convenient to express). There are so many programmers writing asynchronous code just because they need the ability to suspend, e.g. because they need to call some library API that suspends while it waits for other work to complete. In this case, the programmer doesn't actively want parallelism, and they don't need to be confronted with data-race safety errors because they're not trying to pass state across isolation boundaries.

This design decision was made specifically to make the behavior of Task.init consistent across nonisolated synchronous and async functions. We can't change the behavior of unstructured task isolation in nonisolated synchronous functions, because that would require changing the ABI of every unspecified synchronous function. I do not think that's worth it; that's a much larger ABI transition than the one that's currently proposed.

It's also not true that Task.init always inherits isolation in an isolated method. Isolation is only inherited if you capture the isolated parameter in the closure body. The behavior in the proposal more closely matches the existing behavior of task creation in isolated methods.

14 Likes