From c5c794575f3360a5f206c3d223d444d7dd076504 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 14 Jun 2024 12:55:07 +0900 Subject: [PATCH 1/5] Show in-order processing patterns, equivalent to queue.async {} --- Guide.docc/IncrementalAdoption.md | 195 ++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/Guide.docc/IncrementalAdoption.md b/Guide.docc/IncrementalAdoption.md index ee2d15e..73364b5 100644 --- a/Guide.docc/IncrementalAdoption.md +++ b/Guide.docc/IncrementalAdoption.md @@ -227,3 +227,198 @@ NS_SWIFT_ASYNC_NAME NS_SWIFT_ASYNC_NOTHROW NS_SWIFT_UNAVAILABLE_FROM_ASYNC(msg) ``` + +## Dispatch + +### Ordered work processing in actors, when enqueueing from a synchronous contexts + +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: + +``` +// ✅ Guaranteed order, since caller is a single task +let printer: Printer = ... +await printer.print(1) +await printer.print(2) +await printer.print(3) +``` + +This code is structurally guaranteed to execute the prints in the expected "1, 2, 3" +order, because the caller is a single task. Things + +Dispatch queues offered the common `queue.async { ... }` way to kick off some +asynchronous work without waiting for its result. In dispatch, if one were to +write the following code: + +```swift +let queue = DispatchSerialQueue(label: "queue") + +queue.async { print(1) } +queue.async { print(2) } +queue.async { print(3) } +``` + +The order of the elements printed is guaranteed to be `1`, `2` and finally `3`. + +At first, it may seem like `Task { ... }` is exactly the same, because it also +kicks off some asynchronous computation without waiting for it to complete. +A naively port of the same code might look like this: + +```swift +// ⚠️ any order of prints is expected +Task { print(1) } +Task { print(2) } +Task { print(3) } +``` + +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 +threads to schedule the tasks. Because of this, any of the tasks may be executed first. + +Another attempt at recovering serial execution may be to use an actor, like this: + +```swift +// ⚠️ still any order of prints is possible +actor Printer {} + func go() { + // Notice the tasks don't capture `self`! + Task { print(1) } + Task { print(2) } + Task { print(3) } + } +} +``` + +This specific example still does not even guarantee enqueue order (!) of the tasks, +and much less actual execution order. The tasks this is because lack of capturing +`self` of the actor, those tasks are effectively going to run on the global concurrent +pool, and not on the actor. This behavior may be unexpected, but it is the current semantics. + +We can correct this a bit more in order to ensure the enqueue order, by isolating +the tasks to the actor, this is done as soon as we capture the `self` of the actor: + +```swift +// ✅ enqueue order of tasks is guaranteed +// +// ⚠️ however. due to priority escalation, still any order of prints is possible (!) +actor Printer {} + func go() { // assume this method must be synchronous + // Notice the tasks do capture self + Task { self.log(1) } + Task { self.log(2) } + Task { self.log(3) } + } + + func log(_ int: Int) { print(int) } +} +``` + +This improves the situation because the tasks are now isolated to the printer +instance actor (by means of using `Task{}` which inherits isolation, and refering +to the actor's `self`), however their specific execution order is _still_ not deterministic. + +Actors in Swift are **not** strictly FIFO ordered, and tasks +processed by an actor may be reordered by the runtime for example because +of _priority escalation_. + +**Priority escalation** takes place when a low-priority task suddenly becomes +await-ed on by a high priority task. The Swift runtime is able to move such +task "in front of the queue" and effectively will process the now priority-escalated +task, before any other low-priority tasks. This effectively leads to FIFO order +violations, because such task "jumped ahead" of other tasks which may have been +enqueue on the actor well ahead of it. This does does help make actors very +responsive to high priority work which is a valuable property to have! + +> Note: Priority escalation is not supported on actors with custom executors. + +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: + +```swift +// ✅ Guaranteed order in which log() are invoked, +// regardless of priority escalations, because the disconnect +// between the producing and consuming task +actor Printer { + let stream: AsyncStream + let streamContinuation: AsyncStream.Continuation + var streamConsumer: Task! + + init() async { + let (stream, continuation) = AsyncStream.makeStream(of: Int.self) + self.stream = stream + self.streamContinuation = continuation + + // Consuming Option A) + // 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() } + } + + deinit { + self.streamContinuation.finish() + } + + nonisolated func enqueue(_ item: Int) { + self.streamContinuation.yield(item) + } + + nonisolated func cancel() { + self.streamConsumer?.cancel() + } + + func consumeStream() async { + for await item in self.stream { + if Task.isCancelled { break } + + log(item) + } + } + + func log(_ int: Int) { print(int) } +} +``` + +and invoke it like: + +``` +let printer: Printer = ... +printer.enqueue(1) +printer.enqueue(2) +printer.enqueue(3) +``` + +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 +the specific order of items being handled. + +This approach has both up and down-sides, because now the item processing cannot be affected by +priority escalation. The items will always be processed in their strict enqueue order, +and we cannot easily await for their results -- since the caller is in synchronous code, +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 +consuming of the stream. This also may be sub-optimal, because as cancellation must +now be handled manually. You may instead prefer to _not_ create the consumer task +at all in this Printer type, but require that some existing task invokes `await consumeStream()`, like this: + +``` +let printer: Printer = ... +Task { // or some structured task + await printer.consumeStream() +} + +printer.enqueue(1) +printer.enqueue(2) +printer.enqueue(3) +``` + +In this case, you'd should make sure to only have at-most one task consuming the stream, +e.g. by using a boolean flag inside the printer actor. \ No newline at end of file From 68cab913ab5fb2f86926c5166531700806979382 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 14 Jun 2024 13:17:57 +0900 Subject: [PATCH 2/5] Make it a new file about RuntimeBehavior --- Guide.docc/IncrementalAdoption.md | 194 ---------------------------- Guide.docc/MigrationGuide.md | 1 + Guide.docc/RuntimeBehavior.md | 207 ++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 194 deletions(-) create mode 100644 Guide.docc/RuntimeBehavior.md diff --git a/Guide.docc/IncrementalAdoption.md b/Guide.docc/IncrementalAdoption.md index 73364b5..3aa2a7a 100644 --- a/Guide.docc/IncrementalAdoption.md +++ b/Guide.docc/IncrementalAdoption.md @@ -228,197 +228,3 @@ NS_SWIFT_ASYNC_NOTHROW NS_SWIFT_UNAVAILABLE_FROM_ASYNC(msg) ``` -## Dispatch - -### Ordered work processing in actors, when enqueueing from a synchronous contexts - -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: - -``` -// ✅ Guaranteed order, since caller is a single task -let printer: Printer = ... -await printer.print(1) -await printer.print(2) -await printer.print(3) -``` - -This code is structurally guaranteed to execute the prints in the expected "1, 2, 3" -order, because the caller is a single task. Things - -Dispatch queues offered the common `queue.async { ... }` way to kick off some -asynchronous work without waiting for its result. In dispatch, if one were to -write the following code: - -```swift -let queue = DispatchSerialQueue(label: "queue") - -queue.async { print(1) } -queue.async { print(2) } -queue.async { print(3) } -``` - -The order of the elements printed is guaranteed to be `1`, `2` and finally `3`. - -At first, it may seem like `Task { ... }` is exactly the same, because it also -kicks off some asynchronous computation without waiting for it to complete. -A naively port of the same code might look like this: - -```swift -// ⚠️ any order of prints is expected -Task { print(1) } -Task { print(2) } -Task { print(3) } -``` - -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 -threads to schedule the tasks. Because of this, any of the tasks may be executed first. - -Another attempt at recovering serial execution may be to use an actor, like this: - -```swift -// ⚠️ still any order of prints is possible -actor Printer {} - func go() { - // Notice the tasks don't capture `self`! - Task { print(1) } - Task { print(2) } - Task { print(3) } - } -} -``` - -This specific example still does not even guarantee enqueue order (!) of the tasks, -and much less actual execution order. The tasks this is because lack of capturing -`self` of the actor, those tasks are effectively going to run on the global concurrent -pool, and not on the actor. This behavior may be unexpected, but it is the current semantics. - -We can correct this a bit more in order to ensure the enqueue order, by isolating -the tasks to the actor, this is done as soon as we capture the `self` of the actor: - -```swift -// ✅ enqueue order of tasks is guaranteed -// -// ⚠️ however. due to priority escalation, still any order of prints is possible (!) -actor Printer {} - func go() { // assume this method must be synchronous - // Notice the tasks do capture self - Task { self.log(1) } - Task { self.log(2) } - Task { self.log(3) } - } - - func log(_ int: Int) { print(int) } -} -``` - -This improves the situation because the tasks are now isolated to the printer -instance actor (by means of using `Task{}` which inherits isolation, and refering -to the actor's `self`), however their specific execution order is _still_ not deterministic. - -Actors in Swift are **not** strictly FIFO ordered, and tasks -processed by an actor may be reordered by the runtime for example because -of _priority escalation_. - -**Priority escalation** takes place when a low-priority task suddenly becomes -await-ed on by a high priority task. The Swift runtime is able to move such -task "in front of the queue" and effectively will process the now priority-escalated -task, before any other low-priority tasks. This effectively leads to FIFO order -violations, because such task "jumped ahead" of other tasks which may have been -enqueue on the actor well ahead of it. This does does help make actors very -responsive to high priority work which is a valuable property to have! - -> Note: Priority escalation is not supported on actors with custom executors. - -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: - -```swift -// ✅ Guaranteed order in which log() are invoked, -// regardless of priority escalations, because the disconnect -// between the producing and consuming task -actor Printer { - let stream: AsyncStream - let streamContinuation: AsyncStream.Continuation - var streamConsumer: Task! - - init() async { - let (stream, continuation) = AsyncStream.makeStream(of: Int.self) - self.stream = stream - self.streamContinuation = continuation - - // Consuming Option A) - // 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() } - } - - deinit { - self.streamContinuation.finish() - } - - nonisolated func enqueue(_ item: Int) { - self.streamContinuation.yield(item) - } - - nonisolated func cancel() { - self.streamConsumer?.cancel() - } - - func consumeStream() async { - for await item in self.stream { - if Task.isCancelled { break } - - log(item) - } - } - - func log(_ int: Int) { print(int) } -} -``` - -and invoke it like: - -``` -let printer: Printer = ... -printer.enqueue(1) -printer.enqueue(2) -printer.enqueue(3) -``` - -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 -the specific order of items being handled. - -This approach has both up and down-sides, because now the item processing cannot be affected by -priority escalation. The items will always be processed in their strict enqueue order, -and we cannot easily await for their results -- since the caller is in synchronous code, -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 -consuming of the stream. This also may be sub-optimal, because as cancellation must -now be handled manually. You may instead prefer to _not_ create the consumer task -at all in this Printer type, but require that some existing task invokes `await consumeStream()`, like this: - -``` -let printer: Printer = ... -Task { // or some structured task - await printer.consumeStream() -} - -printer.enqueue(1) -printer.enqueue(2) -printer.enqueue(3) -``` - -In this case, you'd should make sure to only have at-most one task consuming the stream, -e.g. by using a boolean flag inside the printer actor. \ No newline at end of file diff --git a/Guide.docc/MigrationGuide.md b/Guide.docc/MigrationGuide.md index 7194b4d..37035b3 100644 --- a/Guide.docc/MigrationGuide.md +++ b/Guide.docc/MigrationGuide.md @@ -59,3 +59,4 @@ full code examples, and learn about how to contribute in the [repository][]. - - - +- diff --git a/Guide.docc/RuntimeBehavior.md b/Guide.docc/RuntimeBehavior.md new file mode 100644 index 0000000..28a0613 --- /dev/null +++ b/Guide.docc/RuntimeBehavior.md @@ -0,0 +1,207 @@ +# Runtime Behavior + + +Learn how Swift concurrency runtime semantics differ from other runtimes you may +be familiar with, and familiarize yourself with common patterns to achieve +similar end results in terms of execution semantics. + +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. + +### Ordered stream processing + +### Ordered work processing in actors, when enqueueing from a synchronous contexts + +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: + +``` +// ✅ Guaranteed order, since caller is a single task +let printer: Printer = ... +await printer.print(1) +await printer.print(2) +await printer.print(3) +``` + +This code is structurally guaranteed to execute the prints in the expected "1, 2, 3" +order, because the caller is a single task. Things + +Dispatch queues offered the common `queue.async { ... }` way to kick off some +asynchronous work without waiting for its result. In dispatch, if one were to +write the following code: + +```swift +let queue = DispatchSerialQueue(label: "queue") + +queue.async { print(1) } +queue.async { print(2) } +queue.async { print(3) } +``` + +The order of the elements printed is guaranteed to be `1`, `2` and finally `3`. + +At first, it may seem like `Task { ... }` is exactly the same, because it also +kicks off some asynchronous computation without waiting for it to complete. +A naively port of the same code might look like this: + +```swift +// ⚠️ any order of prints is expected +Task { print(1) } +Task { print(2) } +Task { print(3) } +``` + +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 +threads to schedule the tasks. Because of this, any of the tasks may be executed first. + +Another attempt at recovering serial execution may be to use an actor, like this: + +```swift +// ⚠️ still any order of prints is possible +actor Printer {} + func go() { + // Notice the tasks don't capture `self`! + Task { print(1) } + Task { print(2) } + Task { print(3) } + } +} +``` + +This specific example still does not even guarantee enqueue order (!) of the tasks, +and much less actual execution order. The tasks this is because lack of capturing +`self` of the actor, those tasks are effectively going to run on the global concurrent +pool, and not on the actor. This behavior may be unexpected, but it is the current semantics. + +We can correct this a bit more in order to ensure the enqueue order, by isolating +the tasks to the actor, this is done as soon as we capture the `self` of the actor: + +```swift +// ✅ enqueue order of tasks is guaranteed +// +// ⚠️ however. due to priority escalation, still any order of prints is possible (!) +actor Printer {} + func go() { // assume this method must be synchronous + // Notice the tasks do capture self + Task { self.log(1) } + Task { self.log(2) } + Task { self.log(3) } + } + + func log(_ int: Int) { print(int) } +} +``` + +This improves the situation because the tasks are now isolated to the printer +instance actor (by means of using `Task{}` which inherits isolation, and refering +to the actor's `self`), however their specific execution order is _still_ not deterministic. + +Actors in Swift are **not** strictly FIFO ordered, and tasks +processed by an actor may be reordered by the runtime for example because +of _priority escalation_. + +**Priority escalation** takes place when a low-priority task suddenly becomes +await-ed on by a high priority task. The Swift runtime is able to move such +task "in front of the queue" and effectively will process the now priority-escalated +task, before any other low-priority tasks. This effectively leads to FIFO order +violations, because such task "jumped ahead" of other tasks which may have been +enqueue on the actor well ahead of it. This does does help make actors very +responsive to high priority work which is a valuable property to have! + +> Note: Priority escalation is not supported on actors with custom executors. + +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: + +```swift +// ✅ Guaranteed order in which log() are invoked, +// regardless of priority escalations, because the disconnect +// between the producing and consuming task +actor Printer { + let stream: AsyncStream + let streamContinuation: AsyncStream.Continuation + var streamConsumer: Task! + + init() async { + let (stream, continuation) = AsyncStream.makeStream(of: Int.self) + self.stream = stream + self.streamContinuation = continuation + + // Consuming Option A) + // 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() } + } + + deinit { + self.streamContinuation.finish() + } + + nonisolated func enqueue(_ item: Int) { + self.streamContinuation.yield(item) + } + + nonisolated func cancel() { + self.streamConsumer?.cancel() + } + + func consumeStream() async { + for await item in self.stream { + if Task.isCancelled { break } + + log(item) + } + } + + func log(_ int: Int) { print(int) } +} +``` + +and invoke it like: + +``` +let printer: Printer = ... +printer.enqueue(1) +printer.enqueue(2) +printer.enqueue(3) +``` + +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 +the specific order of items being handled. + +This approach has both up and down-sides, because now the item processing cannot be affected by +priority escalation. The items will always be processed in their strict enqueue order, +and we cannot easily await for their results -- since the caller is in synchronous code, +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 +consuming of the stream. This also may be sub-optimal, because as cancellation must +now be handled manually. You may instead prefer to _not_ create the consumer task +at all in this Printer type, but require that some existing task invokes `await consumeStream()`, like this: + +``` +let printer: Printer = ... +Task { // or some structured task + await printer.consumeStream() +} + +printer.enqueue(1) +printer.enqueue(2) +printer.enqueue(3) +``` + +In this case, you'd should make sure to only have at-most one task consuming the stream, +e.g. by using a boolean flag inside the printer actor. \ No newline at end of file From 6791bf8076d5d93e9386d1265c7a7e63e6a14f7d Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 14 Jun 2024 20:09:38 +0900 Subject: [PATCH 3/5] Update Guide.docc/RuntimeBehavior.md Co-authored-by: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> --- Guide.docc/RuntimeBehavior.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guide.docc/RuntimeBehavior.md b/Guide.docc/RuntimeBehavior.md index 28a0613..b623966 100644 --- a/Guide.docc/RuntimeBehavior.md +++ b/Guide.docc/RuntimeBehavior.md @@ -11,7 +11,7 @@ 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. -### Ordered stream processing +## Ordered Processing ### Ordered work processing in actors, when enqueueing from a synchronous contexts From fbb7f9d33f41000b0d273f06f3fa108768a5994b Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 14 Jun 2024 20:13:11 +0900 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> --- Guide.docc/RuntimeBehavior.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Guide.docc/RuntimeBehavior.md b/Guide.docc/RuntimeBehavior.md index b623966..6c89c1b 100644 --- a/Guide.docc/RuntimeBehavior.md +++ b/Guide.docc/RuntimeBehavior.md @@ -20,7 +20,7 @@ as the execution remains in a single Task - this is equivalent to using "a singl thread" to execute some work, but is more resource efficient because the task may suspend while waiting for some work, for example: -``` +```swift // ✅ Guaranteed order, since caller is a single task let printer: Printer = ... await printer.print(1) @@ -29,7 +29,7 @@ await printer.print(3) ``` This code is structurally guaranteed to execute the prints in the expected "1, 2, 3" -order, because the caller is a single task. Things +order, because the caller is a single task. Dispatch queues offered the common `queue.async { ... }` way to kick off some asynchronous work without waiting for its result. In dispatch, if one were to @@ -190,7 +190,7 @@ getting processed. Notice that we kick off an unstructured task in the actor's initializer, to handle the consuming of the stream. This also may be sub-optimal, because as cancellation must now be handled manually. You may instead prefer to _not_ create the consumer task -at all in this Printer type, but require that some existing task invokes `await consumeStream()`, like this: +at all in this `Printer` type, but require that some existing task invokes `await consumeStream()`, like this: ``` let printer: Printer = ... From 77824af89818aa642962989e483fbeb36bf40ae1 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 14 Jun 2024 20:13:47 +0900 Subject: [PATCH 5/5] Update Guide.docc/RuntimeBehavior.md Co-authored-by: Matt Massicotte <85322+mattmassicotte@users.noreply.github.com> --- Guide.docc/RuntimeBehavior.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Guide.docc/RuntimeBehavior.md b/Guide.docc/RuntimeBehavior.md index 6c89c1b..8ce435b 100644 --- a/Guide.docc/RuntimeBehavior.md +++ b/Guide.docc/RuntimeBehavior.md @@ -13,7 +13,7 @@ them while you migrate your code to Swift concurrency. ## Ordered Processing -### Ordered work processing in actors, when enqueueing from a synchronous contexts +### From Synchronous Contexts 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