[swift-evolution] Contextualizing async coroutines
Joe Groff
jgroff at apple.com
Thu Sep 7 08:55:31 CDT 2017
> On Sep 2, 2017, at 12:07 PM, Chris Lattner <clattner at nondot.org> wrote:
>
> On Aug 31, 2017, at 11:35 AM, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
>> The coroutine proposal as it stands essentially exposes raw delimited continuations. While this is a flexible and expressive feature in the abstract, for the concrete purpose of representing asynchronous coroutines, it provides weak user-level guarantees about where their code might be running after being resumed from suspension, and puts a lot of pressure on APIs to be well-behaved in this respect. And if we're building toward actors, where async actor methods should be guaranteed to run "in the actor", I think we'll *need* something more than the bare-bones delimited continuation approach to get there. I think the proposal's desire to keep coroutines independent of a specific runtime model is a good idea, but I also think there are a couple possible modifications we could add to the design to make it easier to reason about what context things run in for any runtime model that benefits from async/await:
>>
>> # Coroutine context
>>
>> Associating a context value with a coroutine would let us thread useful information through the execution of the coroutine. This is particularly useful for GCD, so you could attach a queue, QoS, and other attributes to the coroutine, since these aren't reliably available from the global environment. It could be a performance improvement even for things like per-pthread queues, since coroutine context should be cheaper to access than pthread_self.
>>
>> For example, a coroutine-aware `dispatch_async` could spawn a coroutine with the queue object and other interesting attributes as its context:
>>
>> extension DispatchQueue {
>> func `async`(_ body: () async -> ()) {
>> dispatch_async(self, {
>> beginAsync(context: self) { await body() }
>> })
>> }
>> }
>
> I think it makes perfect sense to add a magically available context to async functions, and something like the above is a good way to populate it. Because is is a magic value that is only available in async functions, giving it a keyword like asyncContext might make sense. That said, I don’t understand how (by itself) this helps the queue hopping problem.
By itself, it doesn't, but the context can carry information from when the coroutine was spawned to functions that yield control from the coroutine, so that there's an opportunity to use the context information to schedule the continuation properly up-front, avoiding the need
>> # `onResume` hooks
>>
>> Relying on coroutine context alone still leaves responsibility wholly on suspending APIs to pay attention to the coroutine context and schedule the continuation correctly. You'd still have the expression problem when coroutine-spawning APIs from one framework interact with suspending APIs from another framework that doesn't understand the spawning framework's desired scheduling policy. We could provide some defense against this by letting the coroutine control its own resumption with an "onResume" hook, which would run when a suspended continuation is invoked instead of immediately resuming the coroutine. That would let the coroutine-aware dispatch_async example from above do something like this, to ensure the continuation always ends up back on the correct queue:
>
> Yes, we need something like this, though I’m not sure how your proposal works:
>
>> extension DispatchQueue {
>> func `async`(_ body: () async -> ()) {
>> dispatch_async(self, {
>> beginAsync(
>> context: self,
>> body: { await body() },
>> onResume: { continuation in
>> // Defensively hop to the right queue
>> dispatch_async(self, continuation)
>
> If I’m running on a pthread, and use "someQueue.async {…}”, I don’t see how DispatchQueue.async can know how to take me back to a pthread. If I understand your example code above, it looks like the call will run the continuation on someQueue instead.
That's the intended behavior. `someQueue.async` never "runs on a pthread", it spawns a new coroutine that always runs on `someQueue`, just like the closure-based form always runs a synchronous block of code on `someQueue` today. You would need some other API to spawn a coroutine that schedules onto a specific thread using something other than dispatch.
> That said, I think that your idea of context pretty much covers it: a non-async function cannot have any idea whether it is run on a queue or thread, but there is also no language way to call an async function from a non-async function. I think this means that beginAsync and DispatchQueue.async will have to define some policy: for example, the implementation of DispatchQueue.async could use its own internal data structures to decide if the current task is being run on some dispatch queue (using the maligned “give me the current queue” operation), and ensure that it returns to the originating queue if it can find one.
That's what I was going for.
-Joe
More information about the swift-evolution
mailing list