[Pitch #2] Async/await

That's the purpose (one of the purposes) of Task.runDetached, which is defined in: https://github.com/DougGregor/swift-evolution/blob/structured-concurrency/proposals/nnnn-structured-concurrency.md#detached-tasks

There's also @asyncHandler which will be getting it's own proposal soon AFAIR.

Thanks, I suspected it might be elsewhere. It might be good to include a mention/link in this proposal.

1 Like

The test isn't completed until the task completes, whether it's via a successful return or a thrown error. For your test function to be correct, it needs to be marked async. XCTest as it exists today won't be able to call async functions, so it would have to change to support testing async functions.

Doug

Ah, right. Thanks!

This means I was wrong when I said this:

Apologies if I missed this in previous conversation. If I am wrong, then given in the lifetime of an app, particularly on the Apple platforms, how do we expect to call the first async function if none of the entry points provided by the APIs are presently marked async?

For example, an iOS app. It's common to kick off some async work in application did finish launching. But I can't await an async function in that delegate method because it's not marked as async.

So is Apple just going to sprinkle async in every delegate method when this proposal is accepted and landed? Will we have to something similar for our own?

Or will there be an object we can use whose scope encapsulates async functions? Perhaps this addressed in the Structured Concurrency proposal.

Kiel

This is covered by other proposals, but I added a "future directions" reference here in this proposal because it keeps coming up. @ktoso already noted the use of Task.runDetached to create new detached tasks (which works in synchronous code).

A number of delegate methods will be imported as async based on the Objective-C Interoperability for Concurrency proposal.

Please follow the link about Task.runDetached.

Doug

Thanks for patiently pointing those out. There's lots to take in and I imagine lots to respond to. Thank you.

If a function is both async and throws , then the async keyword must precede throws in the type declaration. This same rule applies if async and rethrows .

Rationale : This order restriction is arbitrary, but it's not harmful, and it eliminates the potential for stylistic debates.

I still find this underjustified and believe these kind of things are best enforced by a linter. There are dozens of places in the languages where unordered lists could have a prescribed order for no other reason than eliminating stylistic debates, so I don't understand why this case is special. All of the following from the first thread still applies:

If the order must be enforced, I would prefer a general principle to be articulated that can handle expansion. I doubt “async” and “throws” will remain the only two words you can write in this position.

3 Likes

Can I test this implementation from a main.swift file? When I try this following code I get a "SIL verification failed: cannot call an async function from a non async function" error.

func foo() async -> Int {
    return 3
}

let a = await foo()
1 Like

Top-level code is not an asynchronous context according to this proposal, so no, you cannot currently do this. I'll clarify the proposal text in this regard.

We do want top-level code to allow this at some point, but it will be in one of the other proposals that depends on this one: Structured Concurrency adds @main support for async, but top-level code is also going to require actor isolation.

Doug

4 Likes

The revised pitch is still ambiguous in its use of “suspension point”. Under Proposed solution: async/await, it says:

[async functions] only give up their thread when they reach what’s called a suspension point, marked by await .

Two paragraphs later under Suspension points, we find (emphasis added):

A suspension point is a point in the execution of an asynchronous function where it has to give up its thread.

If async marks a suspension point, then a suspension point is a place where an async function may give up its thread.

I think clarity on this distinction is fundamentally important to understanding the proposal; as written, the two quoted sections together imply that await has scheduling implications similar to DispatchQueue.async.

What is the simplest asynchronous context where I can start playing with async/await?

Can you spell out what you're concerned about?

At first I thought you were "just" pointing out an inconsistency between may/must statements, but you ended up referring to "scheduling implications". What implications (and why do they matter or why are they surprising)?

FWIW, I read the first quoted sentence as:

[async functions] can only give up their thread when they reach what’s called a suspension point, marked by await .

and the second one as:

A suspension point is a point in the execution of an asynchronous function where it gives up its thread, when it has to.

This is the distinction between a suspension point (where the async function gives up its thread) and a potential suspension point (where it is possible that the caller gives up its thread). await is used to mark potential suspension points.

The original async/await didn't make this distinction, and I just found a small number of places in the document where we still didn't. I've fixed those in [SE-0296] Fix a few missing "potentials" with suspension points. by DougGregor ¡ Pull Request #1221 ¡ apple/swift-evolution ¡ GitHub. Does that clarify the semantics for you?

Doug

1 Like

It depends on your definition of "playing with" :)

If you're happy to write code against the model and see how it type-checks to get a feel for things, then grab a main snapshot from swift.org and go ahead. The type checker implementation (behind -Xfrontend -enable-experimental-concurrency) is very close to what's in the proposal, with a few stragglers for recent changes.

If you want to actually execute some code, that's messier, because the concurrency runtime is still coming up. You can use the hacks from our examples (e.g., https://github.com/apple/swift/blob/main/test/Concurrency/Runtime/future_fibonacci.swift) to launch async tasks via a concurrent Dispatch queue.

Doug

3 Likes

The precise term would be hope :grin:

2 Likes

There are undoubtedly some uses for reasync , such as the ?? operator for optionals, where the async implementation degrades nicely to a synchronous implementation:

Ok, this is a little peculiar, but we might be able to make do with simple ?? overloading. So you can overload functions differing by throws on the argument:

func +(_: Int, _: () throws -> ()) throws -> String { "Throw" }
func +(_: Int, _: () -> ()) -> String { "No throw" }

They properly type-check and the compiler chooses the correct overload (as it should). So maybe we can simply add

func ??<T>(
  _: T?, _: @autoclosure () async throws -> T
) async rethrows -> T { ... }

Though I'm not sure if we should allow the first + pair in the first place :thinking:.

I was already clear on the semantics, but I think the current text is still confusing, or indeed actively misleading. The first sentence I quoted:

[async functions] only give up their thread when they reach what’s called a suspension point, marked by await .

is the first mention of suspension points in the text, and I feel it’s most easily understood as implying that async functions yield time on every await. Since it’s the introduction of the concept, that potentially primes the reader to misunderstand (especially if they skim over things they believe they already understand).

It seems to me that quite a bit of confusion and criticism of the concurrency pitches hinges on this misunderstanding, so it’s probably worth wordsmithing a bit rather than just inserting “can”. Perhaps:

In fact, asynchronous functions never just spontaneously give up their thread; the only time this can happen is when the function reaches a potential suspension point, marked by await.

However, the rest of the paragraph, and the following section called Suspension points, also fail to make the distinction clear. Overall, the “Proposed solution” section doesn’t adequately describe the solution you actually intend to implement.

One of the changes in pitch #2 is that overloading purely on async is disallowed, so this won’t work. (For normal functions I think you can work around this using an extra phantom: Void = () argument, but that doesn’t work for operators.)

Instead, we'd want to match the behaviour of throws. If that's the case, they're distinct types from the second variable, not the result type.

If the + pair is undesirable, someone should file a bug report. However, having subtype in both the argument and return type positions make the overloads unrelated (not proper subtype), so that may not be the case.

It has a lot more to do with what giving up threads means rather than whether it happens. Many people assumed that, when the function gives up their thread, other functions still cannot take control over the data that the first function holds (no interleaves).

Though I do agree that we could be rather precise with the usage of can/may with no loss with

  • Function may give up threads at the suspension point
  • It may choose not to do so as optimization.

The function’s local variables are still its own, so for examples such as those in the document - which downloads an image in to a local, and stores all its intermediate state within its own stack, there’s no problem.

Interleaving concerns are more of an actor thing than an async/await thing, because the model of writing instance methods on a type gives you similar expectations about instance variables as you get for local variables — actors don’t give you that, but the code looks so similar to regular, procedural code. I don’t think developers generally have those expectations about globals or shared references, which are the ways async functions outside of actors could observe suspension.

If potential suspension points within an actor were louder and more explicit, like completion handler-driven code, I expect there would be less concern about interleaving. Maybe it’s just something people will need time to get used to.