Initiating asynchronous work from synchronous code

I like @detached, although "@detached await" sounds a bit odd :)

I guess there should also be something like @async, then?

To avoid a closure, which I think makes a lot of sense to avoid "magic", you could also use a scope like:

@MainActor func saveResults() {
  view.startSavingSpinner()    // executes on the main actor, immediately
  @async do {
     await      // hop to ioActor to save
     view.stopSavingSpinner()  // back on main actor to update UI
1 Like

What exactly are the differences between the proposed functions async and asyncDetached?

1 Like

The desired properties are what Doug lists in the initial post:

So it is:

  • carrying metadata (priority, task-local values, ..), unlike a detached task
  • inheriting executor and actor isolation, unlike detached task
  • no need to self. since we know it is unlikely to cause cycles as the closure will not be retained, and is scheduled for execution immediately

And of course the fact that an async does not return a handle, unlike detaching, since it is aimed for synchronous code where one could not await on the task result anyway.

1 Like

Okay, if the only differences are the return type and that async makes some things implicit which asyncDetached requires to be explicit, then it seems clear that these are “really” (conceptually) the same function.

One of them provides knobs and hooks to customize the behavior, and the other is streamlined for ease of use in the common case.

It is well-precedented in Swift that multiple functions with the same purpose but different options should share the same name, and I think that applies here too.

In the common case where the details are implicit, we want people to write simply:

async {

And in the advanced case where more things need to be explicit, the name of the call should not change, just the details should be added:

let myTask = async(priority: .userInitiated) { @MainActor in

This is very similar to the way closures already allow extraneous details to be omitted, while also permitting those details to be spelled out explicitly when desired.

Extending that same idea to async tasks will make the programming model easier to learn and use, by matching expectations from existing Swift precedent and simplifying the surface area of the new feature.


I'm sorry but I'lll have to disagree on that.

You would not want "the same" function to suddenly type check differently because it had some some extra parameter passed in saying "yeah, do inherit actor context after all".

These type check property:

actor A { 
  func a() { }
  func b() { 
    async { // actor context is inherited, i.e. this closure is isolated to the same actor
      await something()
      // implicitly hops back to the self actor
      a() // on the specific self instance of the A actor

While the same "shape" should not type check correctly with a detached task, because it is concurrent and not isolated to the creating actor:

actor A { 
  func a() { }
  func b() { 
    detach { // this executes in an independent task, on global concurrent executor
      await something()
      a() // error: cannot call actor-isolated function; function is async, insert await here
      await self.a() // this is ok, and is an explicit hop back

Also with regards to task-locals; there initially was a request from instrumentation teams and the server community to allow tracing through a detached task. We since came to the realization that's the wrong question to ask, as one wants not only the locals, but also priority, and the executor, and potentially more things yet to be defined.

Would we have to add a configuration option for every single of those? Or should we bake in the right pattern for people to trust and follow. Leaving detaching to be just that, a detached independent task.

Therefore, I do not agree these two should be spelled the same way.

1 Like

You're half right, some of the distinction is due to contextual inference magic at compile time, but some of the proposed semantics are additionally in fact down to a "different function"'s runtime behavior. async and asyncDetached differ at runtime in that the former will propagate most of the same task context that a child task would, including task locals, priority, and such, whereas asyncDetached creates a whole new independent task that does not inherit anything at all. At compile time, the call site context influences the passed-in closure to async to additionally inherit the actor context of the outer scope as well, so that the closure's implementation hops back to that actor between cross-actor calls.


This is a really good question. I’m not sure I’m sold on the @detached await syntax proposal that follows, but it’s a really good question.

1 Like

This is an important bit of functionality needed to complete the concurrency model. One thing I'm a bit nervous about is the implicit-self semantics. We already live in a world where self is implicit if it is explicitly captured by the closure or if self is a struct or enum, and now we would be adding "if the closure is the argument to the async function". It feels uncharacteristic of Swift to have such special-cased behavior. It feels like we should come up with a more formal semantic for when self is implicit, and until we do so explicitly capturing self { [self] in … } seems like a small price to pay.
If this truly is a special case, it feels to me to have more in common with a do block than a closure (this was mentioned upthread). Something like do async { … } might avoid questions like "how do I get my API to capture self implicitly?" (and has some nice symmetry with func foo() async). If we stay in closure-land we should think about the core reason why implicitly capturing self is OK here, and replace the special-case semantics with a verbose-but-explicit attribute on the closure. Is it because the closure has a guaranteed bounded lifetime, even though this could cause self to outlive the call to async? Maybe @guaranteedBoundedLifetime, though likely we can come up with something better.
Finally, I don't feel like naming is the best way to distinguish the detached and non-detached versions. Something like async(priority: AsyncPriority = .implicit, detachFromCurrentTask: Bool = false, operation: …) feels a but more Swifty to me.


This seems to fix @Douglas_Gregor ’s earlier objection to having something like async (previously proposed as asynchronous) because it avoids the problem of there being in-async and below-async code paths SE-0296: async/await - #89 by Douglas_Gregor

Omitting the handle precludes cancellation, though. I'd rather keep the handle, if only to retain possibility of cancellation.


async and asyncDetached looks good, which represent context-aware-local-attached and context-independent-global-detached async code launch from sync context.

It'd be better they could be merged into async overloads, async {} /*attached*/ and async(...) {} /*detached*/, maybe need some compiler magic to help.

I don't think I get the suggestions to merge async() and detach() into one method. If we did that, and you e.g. specified a priority, then you'd suddenly jump into the semantics of detach() and also lose the actor context etc.? That would be pretty bad, as I see it.

1 Like

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                // hop to ioActor to save
  if let backupServer = backupServer {
    await                // 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."



That sounds good to me, yeah :+1:


Or Task.Handle<Never, Never>, if we don't want to allow get() at all but still want to allow for manual cancellation.


Cool. It might be a good idea to add a Future Directions about making these supported features.

I’m comfortable saying that you need to mark both, the second @detached await is effectively a normal await if they both execute, and the fact that we might not ever detach is more of a feature than a bug. But I don’t know enough about the implementation to know if that just casually makes life horrible for the compiler.

(Weird third alternative: Something like async return x can appear in the middle of a function body, meaning “return x to the caller here, but run the rest of this function body semi-detached.”)

I had to go back and find the definition of detached tasks in the Structured Concurrency proposal to see what you mean, but now I do.

It seems to me that what you’re proposing is a third kind of task, one which inherits many parts of the parent task (like a child task) but has unscoped execution (like a detached task). Rather than introducing a very limited third task type, it makes me wonder if detached tasks should inherit more by default but have affordances to turn it off. For instance, maybe they should inherit priority unless overridden, and actors should have a nonisolated detach method that runs the closure on the actor. (For clarity, the current global detach function might become a static method on Task.)


Oh, I forgot to mention the other thing with marking the await rather than having something like the proposed async. Each await that is potentially @detached also acts as an implicit return from the synchronous function, returning the flow of control to the caller. The implicit return goes against Swift's precedent of trying to mark all of the control flow, which was recently re-affirmed with (e.g.) preventing defer from having asynchronous calls in it because that would introduce implicit control flow.

This also means that @detached await would only be permitted in functions that (1) have a Void return type and (2) are non-throwing.


1 Like

I'm not sure I fully understand why detached needs a prefix. It's a pretty absolute word. I'd be happy if it was left alone.

But if it needs to be specialized I think async should also be specialized to asyncLocal or something like that. Just a nit pick.

+1 for the addition of async as a feature.

Good morning everyone,
the core team has asked we pull this feature into the structured concurrency proposal and run another review there. In a way, this feature is closer to structured concurrency than detach itself which was part of it to begin with :slight_smile:

I'll work on merging the two texts and we'll be able to give this a proper review as part of that proposal shortly.


Yet another muddled question as the work of folding this into the structured concurrency proposal moves along:

Muddled question part 1:

The concurrency roadmap’s glossary says, “All asynchronous functions run as part of some task.” What task does the body of an async { } run as part of?

Does async { } create a child task? I think it does not: the proposal introduces it as sugar for the most common use cases of the previously proposed detach { }, and nothing I can see in the proposal says that async { } bounds the execution of its containing task.

Then does async { } create a new detached task? It must, but it seems from the name that async { } would be non-detached in contrast to asyncDetached { }….

Muddled question part 2:

Assuming that async { } does indeed create a detached task, and creating a child task is thus not the most common case that deserved the simplest sugar, then…why not? For instance, the pitch’s motivating example of saveResults seems like it ought to spawn a child task; surely we want any containing task not to indicate completion until we’ve actually saved those results?

(Apologies if this is painfully obvious or already addressed; I haven’t kept up with all the concurrency proposals to date.)