Initiating asynchronous work from synchronous code

They have much the same purpose and semantics, yes.

No, there is no equivalent. If you need to update application state, do so by having your task hop over to the main actor (covered by the global actors pitch) to perform the updates in a coordinated way.

That's actually the way they are implemented, as two hidden parameter attributes, and I thought it would be more confusing to present them this way and I care more about the overall semantics of async. The two hidden attributes are:

  • @_inheritsActorContext: when a closure argument is provided to a parameter that has this attribute, the closure inherits the actor context in which the closure is created.
  • @_implicitSelfCapture: self can be implicitly captured without requiring self..

We could do these attributes as separate things, now or some time in the future.

No, the closure just ends up being non-isolated.

We went pretty far down this design path. The problem with it is that there may be multiple awaits, and it isn't clear which one is the one that performs the detach:

@MainActor func saveResults() {
  view.startSavingSpinner()                     // executes on the main actor, immediately
  if somethingChanged {
    await ioActor.save()                // hop to ioActor to save
  }
  if let backupServer = backupServer {
    await backupServer.save()                // hop to backupServer to save
  }
  view.stopSavingSpinner()                      // back on main actor to update UI
}

Which of those two awaits should be marked with @detached? The first one? The second? Both? We might not even detach ever, depending on those if conditions.

(Side note: it's also not actually a "detach", because we're inheriting priority, task locals, etc.)

asyncDetached cannot necessarily express all of the things that async does. For example, it's not easy to capture all of the task-local values in the currently-executing task to copy them over, and there are things the OS might want to propagate that through async that won't be expressed in the signature of asyncDetached.

I don't think we can collapse async and asyncDetached into a single function. They are different functions that share a root, async, because they are both initiating asynchronous code.

The requirement to write self. when self has been captured in a closure has a specific purpose: it is to indicate that the developer might need to think about reference cycles. Where it is not likely to be indicative of a reference cycle, the self. requirement creates visual noise, and that noise de-values the usefulness of self. to indicate potential reference cycles. We've been chipping away at the self. requirement over the years in places where reference cycles are unlikely, and this is following that precedent. It's unlikely that we'll get a reference cycle from a closure that is executed and is only held on to by that task.

This is a reasonable point. We could return a Task.Handle<Void, Never>, which would allow cancellation but would not communicate any information back on get() that "it's done."

Doug

7 Likes