-
Notifications
You must be signed in to change notification settings - Fork 24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Show in-order processing patterns, equivalent to queue.async {} #69
base: main
Are you sure you want to change the base?
Conversation
7ef91aa
to
68cab91
Compare
Swift's concurrency model with a strong focus on async/await, actors and tasks, | ||
means that some patterns from other libraries or concurrency runtimes don't | ||
translate directly into this new model. In this section, we'll explore common | ||
patterns and differences in runtime behavior to be aware of, and how to address | ||
them while you migrate your code to Swift concurrency. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think runtime behaviour
is a great article name but IMO we should talk about executors and executor jobs here first and how they relate to Tasks
before we dive into implementing a specific pattern of ordered processing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'm not writing those in "final document order";
we'll do #59 which will explain executors, and it will be here; no worries.
``` | ||
|
||
This example **does not** guarantee anything about the order of the printed values, | ||
because Tasks are enqueued on a global (concurrent) threadpool which uses multiple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would help if we spoke about executors here first IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking a lot about this. Many readers are going to be seeing these concepts for the first time. I find it easier to see concepts I work with in day-to-day code first, and then have access to lower-level details should I need them. Structuring these kinds of things is tricky. But, I think starting high-level and then moving lower could help offer more progressive disclosure. But, it also could make for a less organized document. So, I'm not sure what the correct trade-off is here, especally given that this is specifically about runtime behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's sections to be written about executors, that'd address this concern imho; it would not be in this section anyway
The safest and correct way to enqueue a number of items to be processed by an actor, | ||
in a specific order is to use an `AsyncStream` to form a single, well-ordered | ||
sequence of items, which can be emitted to even from synchronous code. | ||
And then consume it using a _single_ task running on the actor, like this: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have to call out here that this pattern is good to get order but it is potentially dangerous since it can queue up an unbounded amount of work. We should call this out here that AsyncStream
has no external backpressure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I can do a small note and direct to docs; there's a risk in too much information here, so I'll do just a short note on it
// Start consuming immediately, | ||
// or better have a caller of Printer call `startStreamConsumer()` | ||
// which would make it run on the caller's task, allowing for better use of structured concurrency. | ||
self.streamConsumer = Task { await self.consumeStream() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please not put spawning an unstructured task in this example here. Instead let's add a func run() async
that consumes the stream. The users of this actor can then decide on what task to call that run
method on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's explained below. I can inverse the order I guess
// between the producing and consuming task | ||
actor Printer { | ||
let stream: AsyncStream<Int> | ||
let streamContinuation: AsyncStream<Int>.Continuation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this have to be nonisolated
for the example to work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I compiled this with Swift 6 mode, seems fine; I can double check again
so we might need to resort to callbacks if we needed to report completion of an item | ||
getting processed. | ||
|
||
Notice that we kick off an unstructured task in the actor's initializer, to handle the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah we mentioned this down here. Can we just not show the unstructured task to begin with?
Swift concurrency naturally enforces program order for asynchronous code as long | ||
as the execution remains in a single Task - this is equivalent to using "a single | ||
thread" to execute some work, but is more resource efficient because the task may | ||
suspend while waiting for some work, for example: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sentence is quite long, can it be broken down?
``` | ||
|
||
This example **does not** guarantee anything about the order of the printed values, | ||
because Tasks are enqueued on a global (concurrent) threadpool which uses multiple |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking a lot about this. Many readers are going to be seeing these concepts for the first time. I find it easier to see concepts I work with in day-to-day code first, and then have access to lower-level details should I need them. Structuring these kinds of things is tricky. But, I think starting high-level and then moving lower could help offer more progressive disclosure. But, it also could make for a less organized document. So, I'm not sure what the correct trade-off is here, especally given that this is specifically about runtime behavior.
printer.enqueue(2) | ||
printer.enqueue(3) | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think including a real init to make this syntatically valid, and then adding a swift
will make this stand out less than it currently does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok will do
printer.enqueue(2) | ||
printer.enqueue(3) | ||
``` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing here - the lack of syntax highlighting really makes these stand out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, will fix
Co-authored-by: Matt Massicotte <[email protected]>
Co-authored-by: Matt Massicotte <[email protected]>
Co-authored-by: Matt Massicotte <[email protected]>
Huh seems I didn't follow up here, will do :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this documentation work. I appreciate it. Some typos and corrections I found.
printer.enqueue(3) | ||
``` | ||
|
||
In this case, you'd should make sure to only have at-most one task consuming the stream, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, you'd should make sure to only have at-most one task consuming the stream, | |
In this case, you should make sure to only have at-most one task consuming the stream, |
We're assuming that the caller has to be in synchronous code, and this is why we make the `enqueue` | ||
method `nonisolated` but we use it to funnel work items into the actor's stream. | ||
|
||
The actor uses a single task to consume the async sequence of items, and this way guarantees |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The actor uses a single task to consume the async sequence of items, and this way guarantees | |
The actor uses a single task to consume the async sequence of items, and this guarantees |
Can we resolve conflicts for this branch? |
And that's not even half the story to be honest... there's even more advanced patterns which_do_ handle structured concurrency better... I'll leave at this example for now.
Future work here:
withCheckedThrowingContinuation
and is more powerful than the async sequence approach but more complicated...Task { [isolated self] in }
but only once it's been accepted. https://github.com/sophiapoirier/swift-evolution/blob/closure-isolation/proposals/nnnn-closure-isolation-control.mdOverall I'd definitely like to include this somewhere here because it comes up all the time, but unsure about where exactly to put it. Open to opinions @mattmassicotte @hborla @xedin
resolves #22