Composing synchronous functionality on top of asynchronous input. Earlier in the thread, I posted a TStream
and ArduousTStream
as an example of the diamond problem, where a library would like to encapsulate a control-flow diamond where one branch is asynchronous and the other synchronous. If that carries unacceptable performance overhead, then they might have to thrust the diamond upon their clients.
Here's a simplified version that synchronously parses values off a local buffer, which is asynchronous refilled:
struct TStream {
// Returns nil when we've exhausted our source
public mutating func next() async -> T? {
if mightNeedRefill { await refillInternalBuffer() }
// ... synchronously vend a T from our internal buffer
}
}
struct ArduousTStream {
public var mightNeedRefill: Bool
public mutating func refillInternalBuffer() async {}
// Returns nil when we've exhausted our source, if the client remembers
// to refill the buffer when it needs refilling. Otherwise might return
// an early nil
public mutating func next() -> T? {
// ... synchronously vend a T from our internal buffer
}
}
If TStream.next()
always suspends execution, even though refilling its internal buffer is an infrequent operation, I'm worried that will thrust many libraries into designing something akin to ArduousTStream
. But, if the overhead is more on the order of shuffling around some registers, slightly worse data locality for the stack, etc., then this would be far more acceptable to vend TStream.next()
.
Furthermore, a definite suspension point won't disappear if TStream.next()
is marked @inlinable
, but (in theory) much of the other overhead could. (If we go the route where suspension points can be sunk into conditionally executed code, then we no longer have the rule that an await
always suspends, rather await
always suspends until inlining unblocks sinking.)