[Pitch #2] Async/await

All make sense, thanks Doug

I guess I still don't really understand the difference here between async actor methods and normal async methods for the purpose of inout handling. Both are interruptable, and both have the same issue with local variable and closures. I think actors are actually a bit safer here because we can ban reference captures in "actor sendability of closures", wherease normal async methods can allow arbitrary recursion through closures without that check.

-Chris

I still think task cancellation should be automatically checked at every suspension point unless it has been disabled using something like Task.withoutInterruption. For the sake of discussion, let's assume that's the design. In that case every suspension point would potentially throw a cancellation error, thus making await naturally imply try. I know this isn't the thread to discuss cancellation policy but I wanted to point out this relationship.

There isn't a difference between async actor methods and normal async methods. Both can have inout parameters. The restrictions we need are on which arguments you can provide, to ensure that you have exclusive access to the argument to the inout even across a suspension. The actor proposal states the restriction for actor-isolated instance members:

Actor-isolated stored properties can be passed into synchronous functions via inout parameters, but it is ill-formed to pass them to asynchronous functions via inout parameters. For example:

func modifiesSynchronously(_: inout Double) { }
func modifiesAsynchronously(_: inout Double) async { }

extension BankAccount {
  func wildcardBalance() async {
    modifiesSynchronously(&balance)        // okay
    await modifiesAsynchronously(&balance) // error: actor-isolated property 'balance' cannot be passed 'inout' to an asynchronous function
  }
}  

This restriction prevents exclusivity violations where the modification of the actor-isolated balance is initiated by passing it as inout to a call that is then suspended, and another task executed on the same actor then fails with an exclusivity violation in trying to access balance itself.

It doesn't matter whether modifiesAsynchronously is on an actor or not; you still can't pass &self.balance to it.

However, it would be fine to pass &myLocalVariable to it so long as we've already ensured that myLocalVariable doesn't have captures that can run concurrently. That's partially implemented as a warning in the experimental concurrency mode, and I promised (somewhere these threads) to bring that into one of the proposals... but haven't done so yet.

Doug

2 Likes

One thing I'm missing in this proposal - await must be used in an asynchronous context, so how do you get into such a context? The only example I can find in the proposal is being inside an async function, which looks like a chicken and egg situation.

1 Like

I'm wondering if someone can confirm what the XCTest / unit testing story is like.

I'd like to call this out to be sure. I expect it is no different because it seems a synchronous function does not complete until all the asynchronous functions it calls have completed, too. But I'm not sure what effect suspension points and surrendering stacks and threads will have, either.

Riffing on the teacher/college example from the pitch, would this work as I expect with XCTest?

func testHireTeacherHappyPath() throws {
    let mockCollege = College(...)
    
    let teacher = await try Teacher(hiringFrom: mockCollege)
    
    XCTAssertEqual(teacher.faculty, "Faculty of Arts")
}

Specifically:

  1. Will the test be suspended for the initialiser to complete before the test completes?
  2. Will some asynchronously thrown error be caught by the test runner?

If not, what changes need to be made to XCTest to accomodate this?

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