Thank you for such a well written and thorough guide!
I agree with everything in it, I just have some questions concerning the performance implications of async
functions outside of actors. Are such functions executor agnostic, that is they can run on the executor of an async calling function without needing a context switch? Would there be a suspension point between two actor-less async
function calls, an actor to actor-less call, vice-versa, etc?
asynchronous functions are able to completely give up that stack and use their own, separate storage . This additional power given to asynchronous functions has some implementation cost
What kinds of costs are we talking about and in what situations? E.g. register copies vs early-exit calls into the Swift runtime vs heap allocations? Where is this separate storage and when is it allocated/initialized/deinitialized/freed?
â (In practice, asynchronous functions are compiled to not depend on the thread during an asynchronous call, so that only the innermost function needs to do any extra work.)â
What is this saying? Could you give an example?
âIf the calleeâs executor is different from the callerâs executor, a suspension occurs and the partial task to resume execution in the callee is enqueued on the calleeâs executor.â
Is this determination statically known, or do we have to consult the runtime?
Asynchronous function types are distinct from their synchronous counterparts
Another benefit of allowing overloading on async
is it works around the situation where a type wants to implement two protocols with similarly named requirements that differ in asynchronicity.
A suspension point must not occur within a defer block.
Does that mean that an await
cannot occur in a defer
block, even if the function is async
? Or just an await
that explicitly hops executors?
A situation I'm concerned about is having an asynchronous source of data from which I fill up a buffer, and then synchronously vend many T
s (until I need to refill the buffer).
struct TStream {
var (buffer, source): (Array<UInt8>, DataSource)
func next() async -> T? {
// Only happens 1/1000 times
if buffer.count < threshhold { await source.refill(&buffer) }
... // Synchronous code
}
}
struct ArduousTSteam {
var (buffer, source): (Array<UInt8>, DataSource)
/// Make sure to call this if `next` tells you to refill,
/// otherwise you'll get a premature `nil` result the
/// next time you call `next`.
func refill() async { await source.refill(&buffer) }
func next() -> (T, callRefill: Bool)? {
... // Synchronous code
}
}
TStream
is a much nicer API, but what is the overhead of next()
being async over burdening the user with the buffering concern ala ArduousTStream
?
If the caller of next()
is async
anyways (whether actor or actor-less), is there overhead for TStream
vs ArduousTSteam
?