SE-0461: Run nonisolated async functions on the caller's actor by default

The isolation context of the caller cannot influence overload resolution. I think the right way to support both forms is by having only one function that runs on the caller's actor, and then in the Concurrency library we add an API for running a given function on the concurrent executor. E.g. something like this

@execution(concurrent)
public func runConcurrently<R>(
  _ operation: @execution(concurrent) () async -> R
) async -> R {
  await operation()
}

This allows people to explicitly move a specific function or closure off of an actor without having to wrap the code in their own @execution(concurrent) function. I've been wanting this sort of API independent of this change, because I think too many people reach for await Task.detached { ... }.value for this purpose, when the unstructured task isn't actually necessary.

Yes!

Just to be clear, the initial semantics of async functions in Swift 5.5 were not data-race safe. Async functions used to use the unsafe inherit executor model, which means they would not switch back to the original executor after any calls to other async functions in the implementation. SE-0338 was motivated by changing the semantics to be safe in a way that did not change the ABI of async functions. I agree that the decision made in SE-0338 to accomplish a sound rule was ultimately the wrong tradeoff, but as you can see in this proposal, the complexity of staging in an ABI change also has real, significant tradeoffs. The benefit of hindsight and years of real world experience with SE-0338 makes the impact of the tradeoffs much more clear, and I don't think we could have easily foreseen these consequences at the time.

9 Likes