[Pitch #2] Async/await

Unfortunately, { async in ... } already has a meaning as a closure that has a single parameter that we name async. It might not happen in practice, but another syntactic micro-optimization on closure syntax that conflicts with an existing syntactic micro-optimization doesn't seem worthwhile to me.

I also suspect the need to write async here will be greatly reduced by the introduction of the implicit conversion from synchronous functions to asynchronous functions.

As @Lantua notes, throws behaves this way. I don't think we should deviate from that.

I'm going to ignore this bike-shed for now ;)

I have thought about it a bit, and noted that inout parameters to async functions are still fine when working with local variables. That's very much in the spirit of value semantics being generally safe.

The problem identified in the actors proposal with long "access" to values passed inout is addressed by banning actor instance members from being passed inout to an async function. I expect we would do the same for local variables that have been captured by a closure/function that "may execute concurrently with" the place where they were defined.

You're absolutely right, I'll move the discussion. Thank you!

I'll clarify that this correct-but-clinical term "storage accessors" covers both properties and subscripts.

Doug

Warning about this is probably necessary QoI; I can see people reaching for this, and if it's parsed as an parameter they'll be very confused.

4 Likes

Would expect make sense as await try combo?

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.

Terms of Service

Privacy Policy

Cookie Policy