SE-0296: async/await

Again, I’m not pursuing any particular design (whereas the proposals are doing so).

I am trying to understand what alternative designs are possible, so that I can compare them with the proposed design.

If the proposal authors have strong reasons for preferring the proposed design over the alternatives (which one assumes they do), it would be useful to document those reasons in the proposal.

The “alternatives considered” section of the proposal at hand does not list any of these alternatives that we’re discussing. This makes it difficult for reviewers to understand why the specifics of the proposed design were selected over any given alternative.

I am confused by this.

The motivation section of the current proposal is dominated by (in fact I believe it consists exclusively of) discussing ergonomic problems with completion handlers. Async/await is presented as a clean replacement for completion handlers.

But completion handlers are (almost by definition) run in a “detached” manner. Some code calls a function which takes a completion handler, and then continues along sequentially. The function that took the handler, meanwhile, does its thing in the background then calls the completion handler later.

The entire purpose of functions that take a completion handler is to enable “detached” or concurrent operation.

It seems strange to propose a replacement for completion handlers, which does not solve the basic problem that completion handlers are designed for, namely running a detached task.

And it seems even stranger to categorize creating a detached task as “the least-important case”.

From my reading of the proposal, its entire motivation is to replace completion handlers, and the primary (sole?) purpose of functions that use a completion handler is to perform a detached task.

• • •

Furthermore, the alternative from my last post is syntax-optimized for the “await” case. The “detached” case requires an explicit “async” notation in that model.

The fact that completion handlers run "detached" is not a feature; it's a consequence of only having language facilities for wrapping up completion code in closures and then scheduling that for background work somewhere else. Let's consider the first example from the proposal:

func processImageData1(completionBlock: (_ result: Image) -> Void) {
    loadWebResource("dataprofile.txt") { dataResource in
        loadWebResource("imagedata.dat") { imageResource in
            decodeImage(dataResource, imageResource) { imageTmp in
                dewarpAndCleanupImage(imageTmp) { imageResult in
                    completionBlock(imageResult)
                }
            }
        }
    }
}

Note how all of the work of the function is done in that deeply-nested stack of closures. Yes, every one of those functions is effectively creating a detached task, because that's the only mechanism we have today.

The whole point of async/await is to take that work that's buried in those nested closures and linearize it, giving it normal control flow, but without losing the asynchrony. Your proposal to have a call to an asynchronous function create a detached task that runs concurrently with the next line of code---with no indication in the source that this is the case---is completely against the goals of async/await because you don't get linearization. In a sense, it's worse than the status quo, because at least today you have closures that help you reason about "this is different code."

Detached tasks are fine when they are needed, but you are assuming that the use of detached tasks in today's completion-handler world imply that they are the common case for async/await. They are not.

It is not possible to consider every objection or every design direction when writing a proposal. In this case, the design directions you're asking us to explore further seem to go against all of the prior art for async/await as well as its explicit design goals. As I noted before, I think this is the wrong direction, for reasons I've explained in several ways. If you wish to effect some change here, you are going to need specific suggestions and examples.

This might be the point where you need to go write some async/await code, whether in Swift or some other language, to get a stronger feel for the model we're going for.

Doug

6 Likes

Okay, this makes sense. It would have helped me understand if the proposal had spelled these things out.

That was the first alternative I described.

The second one did not have that issue, and would use async to demarcate the detached call.

The proposal as written did not impart that understanding to me. Similarly, it also did not impart an understanding of what the common cases for async/await are expected to be.

All the examples show one async function calling another. This is good as far as it goes, but it does not help me understand when and how the authors expect async functions to be used.

As I mentioned, the motivation section only talks about replacing callbacks, and callbacks always run detached.

So if the authors have a different vision in mind for how to use async, it would be helpful for the proposal to elucidate that so others can understand.

I am not asking for “every objection” to be addressed.

I am saying that the proposal should explain why the particular solution in it was chosen, and which possible alternatives were considered. This is standard practice for Swift Evolution.

I would love to write some async/await code.

I would especially love to write some code that uses async/await in the manner intended by the authors of the proposal. And that requires me to understand what that manner is.

I would love to try it in different languages, but I don’t know which languages have similar models to what is proposed. I do not see a section in the proposal which compares and contrasts the proposed implementation with that in other language.

It would be helpful if the authors could provide some information about what they have found works well or not so well in other languages, what other ideas they considered that may not already exist “in the wild”, and why they believe the design choices in the proposal are superior to the other options they considered.

• • •

Adopting a proposal like this is not simply saying “Yes I’d like to have async/await in Swift.”

Rather, adopting this proposal will make a fundamental decision about how asynchronicity will work in all future versions of Swift.

It is not enough to support the idea “in principle”. It is necessary to achieve high confidence that the chosen model is the best that we can come up with.

And frankly, it is not realistic to expect every reviewer to do all the research individually to reach that conclusion on their own.

It is a responsibility of the proposal authors to provide sufficient information, both to help reviewers understand the decisions involved, and to establish confidence that the authors themselves have done the research and reached the best possible solution.

I have no doubt that the authors have done their research, but the proposal in its current form does not adequately communicate what that research was and why it led to this design choice over any other.

The burden of proof rests with the proposal authors to demonstrate that this one specific design is the best that can be found.

4 Likes

Look, not to be blunt, but you write very long posts, the latest few of which indicate that you misunderstand what async/await is about and is trying to solve.

The burden of proof rests with the proposal authors to demonstrate that this one specific design is the best that can be found.

Well, to a certain extent. But it also falls upon the person who is confused to try to read up in the subject.

Maybe you could study how it works in the other languages where it’s implemented first, such as C# where I guess it originated? Or at least be more terse. This is quickly becoming a meta discussion.

3 Likes

We built some toolchains that you can use to try out this feature. There are still a bunch of rough edges, but you can use those to get a sense of how things fit together. Here are a few little sample programs to get you started:

The toolchains are here. They're like the ones from swift.org, so you install them the same way, but newer:

Right now, there's an amusing-but-illustrative difference between macOS and Linux: the Linux toolchains are using a cooperative, single-threaded executor while the macOS one is using Dispatch for parallel execution. It's due to some temporarily layering issues in the implementation, but also shows how the model works both ways. We'll be fixing this bug soon on the Linux side.

Have fun!

Doug

[EDIT: Updated toolchains to versions that enable concurrency by default]

22 Likes

Go ahead. I posted links to toolchains and examples to get started with the Swift implementation.

For the questions you're asking, any language with async/await would suffice. C#, Rust, JavaScript, Python, or Kotlin all have the same basic approach that we're taking.

Doug

2 Likes

Another great example of use is this update replacing callback-based code in NetNewsWire with async/await. It is probably the closest to what an average developer for Apple platforms will experience in their codebase, that is removal of boilerplate and general improvements to code readability.

11 Likes

Yes, exactly.

I have been following these discussions since before the pitch phase. I have read through all the proposals and the concurrency manifesto. I have asked multiple questions along the way.

And despite all of that, I still do not fully comprehend exactly what is being proposed, what problems it is intended to solve, and how it is meant to be used.

I have put in way more effort than any reviewer should have to, in an attempt to understand.

• • •

A proposal ought to be self-contained. It should explain to someone who has never heard of the idea what is being proposed, how it works, and why it should be added to Swift.

It should not assume any experience with the idea in other languages, and it should not expect people to have pre-existing knowledge of what certain keywords mean. That should all be fully explained in the proposal itself, as well as why the proposed design was selected, and what makes it superior to the alternatives.

• • •

Now, it’s possible that I’m just especially dense and bad at understanding things. But experience has shown that is generally not the case.

I may not be the best at coming up with completely new ideas, but when something is explained to me clearly, I’m usually pretty good at understanding it.

Honestly, I’m naturally rather prone to self-doubt, and it has taken a lot of intentional effort to be able to stand up and say, “No, the problem is not on my end. I am fully capable of recognizing when an explanation is incomplete, and the reason I have not grasped this yet is because it has not been explained sufficiently clearly.”

The proposal seems to assume that readers already know what the proposed feature does. And that is not how things should work. It is not fair to reviewers, and it is not fair to everyone who comes along in the future and wants to learn about the feature for the first time by reading the proposal which introduced it.

Again, it is not realistic to ask every reviewer to try a feature in other languages and do the research themselves.

All that stuff should be summarized and explained in the proposal. A reviewer should not need an in-depth understanding of what a keyword means or how a feature works elsewhere, in order to comprehend a proposal for Swift.

The proposal should communicate what the authors have found, and why they selected the proposed design over any other.

4 Likes

What is your evaluation of the proposal?

+1.

The design is rock solid and the proposal itself is well written and thorough. The proposal text, along with the other pending proposals in this space, will serve as an outstanding foundation for understanding Swift’s concurrency model.

I’d eventually like us to consider allowing get-only properties to be async, but I support deferring that consideration. (It’s a nuanced topic on its own given the intersection with lazy properties and property wrappers.)

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

Most certainly.

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

The design feels exactly right to me. The analogies to try-throw provide a solid basis for understanding/teaching the syntactic and sub-typing rules here.
The ultimate test of the design won’t come until we’ve lived with the full Swift concurrency model for some time, but I think this proposal provides a solid foundation.

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

I’m passingly familiar with the C# async/await model. This proposal aligns with the expectations I bring from there.

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

Too much? I’ve read all of the pitches and threads for the full family of concurrency proposals. I’m familiar with the theory and practice of co-routine models.

4 Likes

I have just a few comments:

  • It took me quite a bit thinking to grok how an async initializer would work in practice given Swift's very important constraint of full initialization. Possibly filling out the example might make it a bit clearer.
  • I have a hard time understanding what this means:
    • An async initializer of a class that has a superclass but lacks a call to a superclass initializer will get an implicit call to super.init() only if the superclass has has a zero-argument, synchronous, designated initializer.

  • I'm not sure I follow the section "Overloading and overload resolution" and I think it could be clarified as well.
    • Many such APIs are likely to be updated by adding an async form:

      • What is "updated" mean here? Adding a new overload or replacing the function outright?
    • func doSomething(completionHandler: ((String) -> Void)? = nil) { ... } and func doSomething() -> String { /* ... */ }

      • They don't really match, because with the first one if you call doSomething() nothing gets "returned" in any sense.
    • Instead, we propose an overload-resolution rule to select the appropriate function based on the context of the call. (...later) func doSomething() -> String { /* ... */ } // synchronous, blocking

      • This kinda sounds dangerous, but also not sure if I'm reading it right. Does this imply that when someone updates to a new library all of a sudden a call to doSomething() might block when it wouldn't have previously?

What is your evaluation of the proposal?
As written, overall +.7. I love the approach that the author's are taking overall but I find the above pieces somewhat confusing.

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

Does this proposal fit well with the feel and direction of Swift?
Yes, and I very much appreciate the explicit nature of await and the cohesiveness of all the various proposals.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I think when combined with Tasks and Actors this is (semantically) the best async/concurrency design I've come across. Experience with Kotlin and JS.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I've been lurking and reading since the manifesto 1st edition. Read comments here and the proposal in full.

Normally, the compiler will insert super.init() for you automatically if you don't have any super.init. That won't be the case if super.init() is async.

I'm pretty sure most libraries would need to deprecate the old one first before removing it. So we should have both versions for a while.

You'd want to handle the string passed to the completionHandler though, in much the same way that you'd handle the string returned from the second version. So in that sense, the second version serves the same purpose as the first one, but the second version is in async form (which we deem desirable).

Those two are in different contexts, though.

The first part refers to when you have both the completion-handler version and async version. The compiler would need to infer the appropriate sync-async overload.

The second part refers to the fact that those two functions aren't allowed together. Doing so will be treated as redeclaration.

1 Like

The first part refers to when you have both the completion-handler version and async version. The compiler would need to infer the appropriate sync-async overload.

Ah! I see where I got mixed up now. So that first section is really only comparing these 2 overloads

func doSomething(completionHandler: ((String) -> Void)? = nil) { ... }
func doSomething() async -> String { ... }

and the caller's only 2 choices will therefore be

doSomething()  // in a sync context
or
let x = await doSometing()  // in an async context

Which I think are differentiated enough to be clear.

Am I getting that right?

yup

+1 to this proposal then, though I'm still of the opinion that that section could be made clearer.

Thanks!

1 Like

What is your evaluation of the proposal?

Good. I wish this proposal offered a way to start an async chain because it looks incomplete without it. Reading the other concurrency proposals paints a sufficiently complete picture however.

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

Yes. I find writing completion handlers with proper handling of failure cases to be error prone, and the same is true about running everything in the correct queue. This async/await model will be much more convenient.

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

I think it does. I'm a bit worried transitioning code to use this model isn't going to be very smooth in the short term, but for the long term this is a very good direction.

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

n/a

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

I read and reflected on all the concurrency proposals.

1 Like

Many people may think this.

For the record, I maintain a library where I firmly plan to preserve synchronous APIs alongside the new asynchronous ones. It is just as regular to access an SQLite database synchronously, or asynchrounously. I'll respect SQLite users who expect synchronous apis to be present, just as I'll respect users who want to spawn database accesses off the main thread.

1 Like

There's something important to know about async vs callback (I wrote about this above but it remained unnoticed possibly because I'm missing something):

If I use the callback version of your library call, it means my stack will be unwound immediately and all local variables will be destroyed except those that are captured in the callback, i.e. only the ones I need.

If I use the async version of your library method with await, it means my entire stack with all its resources will remain captured - everything I need and everything I don't.

If this is the case (from my understanding of the underlying mechanisms of async/await) then the advantage of async over callbacks is not as clear cut and to me it would be great to have a choice when working with libraries. Callbacks are a bit more problematic when it comes to error handling and nesting, but they are more clear in how they use local resources.

I gave an example above where you send a large object using a URL task and wait for the result using await: unless you specifically take care of it, the object will remain in memory until the response arrives. Not only that, you may have other dynamically allocated resources down the stack that you needed only in preparation for your URL task, but are not needed once the network call is made.

Anyway, just something to keep in mind but again, if my understanding of the problem is correct.

2 Likes

This is an interesting question. I believe (I have nothing to support it) that this is an implementation detail of the language: maybe the first release will capture everything. Maybe future releases will only capture what is needed. I'm not surprised this is not detailed in the proposal (maybe I did not read carefully enough).

I think (can anyone confirm/deny?) you can use do block for finer-grain control of the object allocation as end-of-scope is the guaranteed latest location for deallocation.

Although it doesn't seem to be part of the current proposal but some sort of "escaping" mechanisms will probably be provided. Task.withUnsafeContinuation was already mentioned above (hmmm shouldn't I generally stay away from everything that has unsafe in the name? :stuck_out_tongue_winking_eye:), also async let as a possible substitute for futures could do the job. But then it looks to me like a rabbit hole of new ways of complicating the code where the benefits of the async/await paradigm become less and less clear.