owned this note changed 2 years ago
Published Linked with GitHub

Reading notes: Async/await in Swift

tags: reading-club

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md

Leave questions, observations, discussion topics below.


Composition of effects

It stands out to me that one of the motivations is: "we added syntax to make error handling nice, but callbacks didn't allow us to reap any benefits from it". Composition of language features seems like a big problem for most languages, and they call it out explicitly here.

Also in terms of composition: it seems they've done the same thing we've done by separating "fallibility" and "asyncness". In JavaScript-style async/await these two are the same: "asyncness" always implies "fallibility" (resolve vs reject in JS). Swift seems to mirror a lot of the choices we've made - at least on the surface.

nikomatsakis: Interesting; I was having a conversation about this on moro. The danger if you ignore errors is that you can spawn a task that yields a Result<(), E> and never realize that it resulted in Err, and hence fail to take down your scope. Making the API more tailored to Result avoids that danger, but it bakes in "error".

yosh: Structured concurrency RFC should talk about this?

tmandry: Swift lets you coerce(?) an infallible function to fallible, so their structured concurrency doesn't have this problem.

func spawn(task: @Sendable () throws -> Void) {}

spawn({}) // infallible
spawn({   // fallible
    if ... {
        throw SomeError();
    }
})

JavaScript

The "pyramid of doom"-style callback nesting is identical to how Yosh used to write async code in JS (:

await covers all subexpressions

nikomatsakis: Really interesting that await covers all async functions "inside" the await, so that e.g.

await session.dataTask(with: server.redirectURL(for: url)) // error: must be `try await`

works even if server.redirectURL is async.

No futures

tmandry: Interesting that they eschewed the Future model completely. You always must have await to call an async function in Swift. A single await can also cover multiple calls in an expression. It marks suspension points but isn't itself a suspension point.

I think this means that all combinator-style interfaces happen on tasks.

guswynn: didnt really understand the motivation here? seem concerned about "futures being big", but the size is fixed based on the compiler-generated generator, regardless of if you are returning a type or building async into the type system?

tmandry: Part of it is consistency with throws and the try effect. Also Swift cares less about performance, more about reliability and predictability and ease of use.

All futures are send?

Because asynchronous functions must be able to abandon their thread, and synchronous functions don’t know how to abandon a thread ()

This seems like perhaps unlike us, Swift doesn't distinguish between concurrency and parallelism: all futures are multi-threaded, so all futures must be thread-safe. This doesn't seem to be the case for non-async Swift tho?

This may be the same point Tmandry makes in the previous section tho :P

tmandry: That's right, but they do some interesting stuff in the actors RFC to propagate execution contexts. Since actors are serial queues this allows you to skip synchronization if you're already running on that queue.

tmandry: Also they can't split mut references to their enclosing context across multiple sub-tasks.

guswynn: question: does swift do anything to prevent overlapping mutable access?

Encourage people to write code using models like actors. Also copy-on-write collections.

Async protocols and closures just work

The swift equivalent of async-fn-in-trait just workslikely because swift is willing to just box as aggressively as possible.

Similarly, async closures are part of this proposal. Again, boxing and what not avoids typical hrtb lifetime problems we see with "closures that return futures" we see in rust.

pnkfelix: recursive functions probably come out of this design as well, as guswynn pointed out to me in zoom chat

nikomatsakis: Would be good to compare and see what problems fall out.

..some discussion around HRTB problems and maybe we know how to fix them now?

Examples wanting to write things like

f: impl Fn(&mut Self) -> impl Future<Output = > + '_
// but this requires generic closures and richer HRTB
// plus the usual "not sufficiently generic" sort of problems that come up, likely from lack of HRTB implied bounds
// same problem we hit on GATs

tmandry: Also Swift Arcs everything if it's shared.

Async operator overloading

As far as I can tell, is scoped to functions, not protocol methods. I (guswynn) am also curious to see how operator overloading plays out in practice. Will async versions of libraries using actors/the structured concurrency proposal be able to keep the same api boundary as sync versions?

niko: Interesting that they think you want map to be parallel/concurrent in an async context. Doesn't sound very robust. Example: Could parse something and get a sequence of commands.

Yosh: wrote a blog post on why concurrent iteration semantics are not the right default for Rust: https://blog.yoshuawuyts.com/async-iteration/

Felix: Map where order matters feels wrong. But would be better to opt in to parallel iteration.

Felix: Guy Steele talk about how iterators are wrong https://www.youtube.com/watch?v=ftcIcn8AmSY or https://www.youtube.com/watch?v=dPK6t7echuA

await motivation is interesting

Marking potential suspension points is particularly important because suspensions interrupt atomicity. 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. A classic but somewhat hackneyed example where this atomicity matters is modeling a bank: if a deposit is credited to one account, but the operation suspends before processing a matched withdrawal, it creates a window where those funds can be double-spent.

Yosh thinks this is a really neat motivation that we might want to consider adapting for our conversations around "why should .await be annotated"?

nikomatsakis: It's interesting because await <expr> is intentionally "non-specific" about where the interruptions can occur.

Select a repo