SE-0296: async/await

Hi Swift Evolution!

The review of SE-0296 — async/await begins now and runs through December 22, 2020.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager (via email or direct message in the Swift forums).

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thanks,
Ben Cohen
Review Manager

39 Likes

Nit:

the bolded part (emphasis mine) is part of structured-concurrency, not this proposal, correct?

FYI: It's not only about async/await, so I made a separate post, but I have raised several issues there that are particularly relevant here:

  • What is the await keyword really for, and when does requiring it help protect the programmer? Are there kinds of code where it's just boilerplate?
  • What will we do about the likely proliferation of needless try's in async code?
3 Likes

You are correct; fixed by [SE-0296] 'async' is not a prefix for 'let' in this proposal. by DougGregor · Pull Request #1231 · apple/swift-evolution · GitHub. Thanks!

Doug

Definitionally, it indicates potential suspension points. These are places where you may give up the thread of execution, which has various user-visible effects, including (1) the resumption might occur "much" later, which is both a potential performance concern as well as throwing off anything you're doing that might involve absolute times; (2) if you're accessing any state where suspension allows interleaving to occur, e.g., if interleaving is allowed for actors; (3) if you're using existing APIs that somehow depend on the actual OS thread you're using, e.g., because they are using thread-local storage.

You probably need to show examples of a "needless" trys introduced by async code. async doesn't require throws, which is why await does not imply try.

Doug

6 Likes

In existing async code, there's already a proliferation of Result<T, Error> when errors need to be propagated. Uses of Result are neatly replaced with the existing try mechanism in combination with the proposed async/await feature. You can get an early taste of the difference it makes in NetNewsWire (as an example) here: Concurrency Upgrades by kavon · Pull Request #1 · kavon/NetNewsWire · GitHub (note that the changes are a work-in-progress)

10 Likes

Doug, thanks for your answer. I think it would be more useful to discuss this stuff in the thread I referenced, where there's more context, which is why I made it a separate thread. I was just trying to preview the issues here.

Thanks in particular for the response about await; I'll have to give what you said there some thought…

I know it isn't implied by the proposed language definition. But as I wrote in the referenced post, the issue is that composable async code requires pervasive cancellability, which in our system means pervasive try… and nearly all instances of try are needless.

The proposal includes these lines:

Instead, think of an asynchronous function as an ordinary function that has the special power to give up its thread. Asynchronous functions don’t typically use this power directly; instead, they make calls, and sometimes these calls will require them to give up their thread and wait for something to happen. When that thing is complete, the function will resume executing again.

It says that an async function doesn’t typically give up its thread directly. This implies that an async function can give up its thread directly.

Is that capability in fact being proposed?

If so, what is the proposed spelling?

When an async function directly gives up its thread, how can it specify when to resume execution?

The proposal says:

For example, if an asynchronous function is running within a given context that is protected by a serial queue, reaching a suspension point means that other code can be interleaved on that same serial queue.

Is this behavior being proposed?

If so, how can it be achieved?

As in, could we see an example (written in Swift using this proposal) of how to create a serial queue which can interleave code when something on it suspends?

My understanding is that it is not a part of this proposal. It maybe be possible to provide an example with this + the Structured Concurrency proposal, but not with plain async/await. Structured concurrency API would give you a way to schedule tasks on queues etc.

Even so, it is clearly a significant motivating use-case, called out directly in the proposal.

In fact, I would say it is the only “interesting” use-case in the proposal. All the other examples show an async function being called essentially synchronously, where the code just waits for the await to complete.

If the intention is to allow interleaving, as it seems to be, then we should see what that will look like.

1 Like
  • What is your evaluation of the proposal?

Finally, it‘s here - I‘m loving it!

  • Is the problem being addressed significant enough to warrant a change to Swift?

Yes, for sure!

  • Does this proposal fit well with the feel and direction of Swift?

Absolutely! (Assuming the feel and direction are to become the best general purpose language with wide adoption out there for the next decade.)

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I used Kotlin Coroutines. This proposals is a much better design IMHO as it doesn’t rely on an IDE that fills in the gaps like marking asynchronous calls but instead builds the ‚await‘ keyword right into the language itself.

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

No sample code written, but read basically all related threads there is on the Swift forums (including most comments).

4 Likes

The presence of await does not mean that the call is synchronous. I think the confusion here, which is a common misunderstanding, is that the "wait" does not block the original thread. That's what "synchronous" would mean. Instead, the "wait" means "if this call needs to do something asynchronously then call me back later when it's finished". At that point the function is suspended, and the function gives up its use of the current thread, which can then move on to do other things.

Later, when that asynchronous work is complete, the function is resumed from where it left off (maybe or maybe not on the original thread). At this point, as with any other call-back-oriented code, the call stack will be different from what it was before the await.

It would help to separate in your mind the concepts of "synchronously" and "serially". The await causes the code to execute serially (which is not necessarily the case with a callback closure), but not synchronously. Synchronous would imply it never gives up control over the thread it's running on, and its call stack never changes. Serial just means the lines of code within that function progress in a way that matches the ordinary control flow you're used to.

12 Likes

Very strong +1.

Yes, 100%. The lack of async/await was sorely missing in Swift for years. It was especially frustrating to use other languages that have this feature present, and then to come back to Swift, which still requires the use of delegate- or callback-based APIs. I really hope that won't be the case anymore.

Yes. The introduced syntax is explicit enough, but introduces great productivity and QoL improvements when working with asynchronous APIs.

I've used both Python and JavaScript extensively, where async/await work mostly the same for end users, with the only major difference being explicit error handling in Swift, which I do like. I also commend the general direction of the multiple concurrency proposals (structured concurrency, actors, AsyncSequence et al), and how well thought out they are. Seems like it was well worth the wait, and Swift is getting a much better support for concurrency than most of the mainstream languages!

I've been tracking discussions following the original concurrency manifesto by Chris Lattner, and some of the async/await threads that these forums saw over the years. I read the current proposal, and the rest of the concurrency proposals.

9 Likes

Strong +1

Absolutely - this is one of the major pain points remaining in the ecosystem, especially for server side applications.

I think so - I particularly like the use of keywords like async and await instead of native Promise implementations like Javascript started with.

I've used futures/promises in Javascript/Typescript extensively and I'm excited to see that this proposal lines up with a lot of the syntax there. I think it reads clearly and is powerful for the user.

Quite a bit - I've been reading the proposals since they first went up a few weeks ago and participating in the discussions, and then of course did another review when this was posted.

5 Likes

Unless I’ve missed something, the proposal does not show any examples of interleaving.

The proposal does not show any examples of the original thread continuing to do other things during the await.

That is what I am asking for examples of.

That seems to be the main purpose and primary motivation for this feature.

And the proposal shows zero examples of it.

The proposal does not adequately communicate how this feature is intended to be used.

Even if the mechanism for actually using the feature in that way has been split off into a separate proposal, it should still be explained here as motivation for this feature, with examples, so that reviewers of this proposal can understand what the purpose is and how it is intended to be used.

1 Like

In the current (pre-async/await) world, the main thread/queue is already asynchronous at the highest level. For example, consider a view controller:

override func viewWillAppear(_ animated: Bool) { … }
override func viewDidAppear(_ animated: Bool) { … }

The second of these is always called after the first, but the timing is asynchronous — there is a time gap (analogous to a suspension point) between the two, during which any other main thread code could execute.

The async/await proposal doesn't really change anything fundamental, except that it introduces suspension points inside functions alongside the existing suspension points between function invocations.

I think @Max_Desiatov is correct that you can't demonstrate the resulting interleaving solely in terms of base async/await syntax. However, there are lots of other ways of executing independent code on the same thread (e.g. using GCD as in the example I gave you over in the other thread).

6 Likes

The proposal under review in its text already links to the structured concurrency proposal. Reviewers needing more context can freely follow the link. What would be the benefit of duplicating examples in two separate, but related proposals?

The proposal presents async/await as an answer to the (numerous) problems identified with the "completion handler" pattern, and the interleaving/"gives up the thread" behavior is already commonplace in these situations. Indeed, that's precisely why you use a completion handler when scheduling UI work to be done after a long-running task: you want to allow other work items to execute on the main thread before the task completes.

Perhaps the proposal could draw this point out a little more explicitly, but I don't think that the concept is quite as foreign as you're contending.

2 Likes

What is your evaluation of the proposal?

I started off as broadly +1, but on more careful reading of the proposal and allowing things to stew a bit, I'm not so sure. It's quite hard to judge without also considering the planned executor/continuation API (which I haven't looked at in as much detail). But even considering the async/await model proposed in this document as a kind of abstract concept, I have concerns with the programming model that I think merit further discussion.

  1. Read-only async properties and subscripts should be allowed. The proposal says:

    Properties and subscripts that only have a getter could potentially be async ... Prohibiting async properties is a simpler rule than only allowing get-only async properties and subscripts.

    My argument against this is similar to Doug's argument against banning inheritance for actors: why impose arbitrary restrictions? Also, currently, functions do not satisfy get-only property requirements in protocols, meaning synchronous types which want to use properties for a more natural interface will also be forced to use functions.

    protocol AsyncCollection {
      func startIndex() async -> Index
    }
    struct MySyncCollection: AsyncCollection {
      // Does not conform to AsyncCollection:
      var startIndex: Index { ... } 
    
      // Instead, they'd have to write this:
      func startIndex() -> Index { ... }
    }
    
    let syncInstance = MySyncCollection()
    syncInstance.startIndex() // function interface forced upon non-async code
    

    Allowing async properties might just be the best interface for both synchronous and async conformers.

  2. Cancellation.

    The proposal tries to defer this discussion, but I believe it is a crucial element of the async/await programming model to say whether or not async functions can be cancelled. If they can be cancelled, it means every possible suspension point is also a possible return point, which is a critical detail that the proposal does not mention at all.

    For example, consider the following code:

    let lock = await getLock()
    let otherData = await downloadOtherData()
    lock.release()
    

    If async functions could not be cancelled, we know that either lock.release() will be called, or that the function never got scheduled again and leaked its whole stack (lock.deinit will never be called). But if they can be cancelled, lock may be destroyed without us calling .release(). Is that obvious from the code? I don't think it is.

    There are potential dragons here if you leave data in an invalid state across an await, and that data is somehow visible to others or (the more subtle case) relied upon in the type's deinitializer. For example, if lock was a buffer that we were filling, we might try to deinitialize invalid memory because we forgot that await meant the function could return in the middle of initializing. And to be fair, when the code looks like: let otherData = await downloadOtherData(), it's hard to spot that there's a hidden return in there. The only clue is on the right-hand side of an assignment operator.

    As far as I can tell, this possible footgun is the same reason _modify still isn't an official language feature (because every yield is also a possible return point). It's important we don't ignore this very important consideration, which developers using async/await in other languages can struggle with.

Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?

It's fine, I guess. I'm a little concerned that we're doing this without a lot of the infrastructure other languages have (coroutines, generators).

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

I've mostly been using Swift since async really started taking the world by storm, so I've missed a lot of that. That said, I have heard criticisms from other developers that async ends up spreading to all areas of your code, and I could see similar things happening here. For example, with synchronous functions able to satisfy async protocol requirements, I could imagine lots of developers will default to making everything async. I'm not sure there is any way to avoid that, or what a better approach would be.

9 Likes