Overall I'm a big +1 on the goals this proposal aims to solve. I think the initial semantics of Swift Concurrency that async methods inherit the isolation of the caller were correct and the system was really sound and easy to understand. The change in SE-0338 was mostly driven by performance concerns and IMO we learned over the past years was the wrong trade-off.
I like how this proposal allows to stage in the change back to the old behaviour and hopefully at some point we can make that the default again. I also love that the proposal finally spells out the function conversion rules.
Having said that I personally really dislike calling the attribute @execution
. Execution and isolation are two orthogonal but related concepts in Swift concurrency and we are mixing them here. Isolation is either in terms of the current actor or the current task and completely decoupled from where the code actually executes. Where code executes is actually a decision tree that takes isolation into account. Currently the decision tree for execution is the following
- Check the isolation
- If the code is isolated we check if the actor has a custom executor
- If the code is nonisolated or on an actor without a custom executor we check if there is a task executor preference
- If nothing applies we run on the global default executor
Now I understand that the proposal is trying to change the semantics of nonisolated
from meaning "definitely not isolated to an actor" to "maybe isolated to an actor". Looking at the alternatives considered section:
One other possibility is to use isolation terminology instead of
@execution
for the syntax. This direction does not accomplish goal 1. in the previous section to have a consistent meaning fornonisolated
across synchronous and async functions. If the attribute were spelled@isolated(caller)
and@isolated(concurrent)
, presumably that attribute would not work together withnonisolated
; it would instead be an alternative kind of actor isolation.
Why would this have inconsistent meaning for synchronous and asynchronous functions?
Having used Concurrency extensively over the past years it would make complete sense to me to call these attributes @isolated(caller)
and @isolated(non)
and allow them to apply to nonsisolated
methods.
While I can see the argument for saying code is running concurrently with the callers actors I think this reasoning falls short on two fronts:
- When the caller itself is nonisolated nothing runs concurrently when calling a proposed
@execution(concurrent)
method - When using task and actor executors nothing might run concurrent at all
Take a simple example like this
@execution(concurrent)
nonisolated func foo() async {
await bar()
}
@execution(concurrent)
nonisolated func bar() async {}
From looking at the code one might assume that bar
now runs concurrently with foo
but that's not the case at all.
I really feel like we shouldn't mix isolation and execution terms here. Right now it is somewhat easy to explain where code is isolated to and where code actually executes. Both have their own decision tree that are related but have separate vocabularies. With the proposed @execution
attributes this becomes way harder to explain.
Similarly, the async function can be described as running on the concurrent executor.
FWIW, I think that we should stop calling it the concurrent executor but rather call it the default executor similar to what the pitch for custom global executor used.
task executor preference can still be used to configure where a nonisolated async function runs. However, if the nonisolated async function was called from an actor with a custom executor, the task executor preference will not apply.
This reads like task executors preference is dropped when in fact the preference is still there it just isn't evaluated since in our execution decision tree we stop at the first check if we are isolated. If at any point further down the call chain we drop the callers isolation the task executor preference will apply again.
I also think that task executors show again how isolation and execution are orthogonal. We might still run on the task executor even if we are isolated to the caller since an actor (without a custom executor) can continue to run on the callers executor.