Skip to content

Commit 70499c5

Browse files
authored
Merge pull request #2505 from rnro/patch-1
Minor clarifications of 0417-task-executor-preference.md
2 parents f183e32 + af74460 commit 70499c5

File tree

1 file changed

+16
-16
lines changed

1 file changed

+16
-16
lines changed

proposals/0417-task-executor-preference.md

+16-16
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
Swift Concurrency uses tasks and actors to model concurrency and primarily relies on actor isolation to determine where a specific piece of code shall execute.
1212

13-
The recent introduction of custom actor executors in [SE-0392](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0392-custom-actor-executors.md) allows customizing on what specific `SerialExecutor` implementation code should be running on while isolated to a specific actor. This allows developers to gain some control over exact threading semantics of actors, by e.g. making sure all work made by a specific actor is made on a dedicated queue or thread.
13+
The recent introduction of custom actor executors in [SE-0392](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0392-custom-actor-executors.md) allows specifying a `SerialExecutor` implementation code should be running on while isolated to a specific actor. This allows developers to gain some control over exact threading semantics of actors, by e.g. making sure all work made by a specific actor is made on a dedicated queue or thread.
1414

1515
Today, the same flexibility is not available to tasks in general, and nonisolated asynchronous functions are always executed on the default global concurrent thread pool managed by Swift concurrency.
1616

@@ -23,7 +23,7 @@ Notably, since Swift 5.7’s [SE-0338: Clarify the Execution of Non-Actor-Isolat
2323
As Swift concurrency is getting adopted in a wider variety of performance sensitive codebases, it has become clear that the lack of control over where nonisolated functions execute is a noticeable problem.
2424
At the same time, the defensive "hop-off" semantics introduced by [SE-0338](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0338-clarify-execution-non-actor-async.md) are still valuable, but sometimes too restrictive and some use-cases might even say that the exact opposite behavior might be desirable instead.
2525

26-
This proposal acknowledges the different needs of various use-cases, and provides a new flexible mechanism for developers to tune their applications and avoid potentially un-necessary context switching when possible.
26+
This proposal acknowledges the different needs of various use-cases, and provides a new flexible mechanism for developers to tune their applications and avoid potentially unnecessary context switching when possible.
2727

2828
## Proposed solution
2929

@@ -473,7 +473,7 @@ func computation(_ int: Int) -> Int {
473473
let eventLoop: MyCoolEventLoop? = EventLoops.find(unownedExecutor)
474474

475475
// Dangerous because there is no structured guarantee that eventLoop will be kept alive
476-
// long for as long as there is any of its child tasks and functions running on it
476+
// for as long as there are any of its child tasks and functions running on it
477477
Task(executorPreference: eventLoop) { ... }
478478
}
479479
}
@@ -484,7 +484,7 @@ func computation(_ int: Int) -> Int {
484484
It is possible to declare a single executor type and have it conform to *both* the `SerialExecutor` (introduced in the custom actor executors proposal),
485485
as well as the `TaskExecutor` (introduce in this proposal).
486486

487-
If declaring an executor that adheres to both protocols like that, it truly **must** adhere to the `SerialExecutor`
487+
If declaring an executor which conforms to both protocols, it truly **must** adhere to the `SerialExecutor`
488488
semantics of not running work concurrently, as it may be used as an *isolation context* by an actor.
489489

490490
```swift
@@ -519,11 +519,11 @@ final class NaiveQueueExecutor: TaskExecutor, SerialExecutor {
519519

520520
Since the enqueue method shares the same signature between the two protocols it is possible to just implement it once.
521521
It is of crucial importance to run the job using the new `runSynchronously(isolatedOn:taskExecutor:)` overload
522-
of the `runSynchronously` method. because it will set up all the required thread-local state for both isolation assertions
522+
of the `runSynchronously` method. This will set up all the required thread-local state for both isolation assertions
523523
and task-executor preference semantics to be handled properly.
524524

525-
Given such executor, we are able to have it both be used by an actor (thanks to being a `SerialExecutor`), as well as have
526-
any structured tasks or nonisolated async functions keep executing on this executor (thanks to it being a `TaskExecutor`):
525+
Given such an executor, we are able to have it both be used by an actor (thanks to being a `SerialExecutor`), and have
526+
any structured tasks or nonisolated async functions execute on it (thanks to it being a `TaskExecutor`):
527527

528528
```swift
529529
nonisolated func nonisolatedFunc(expectedExecutor: NaiveQueueExecutor) async {
@@ -596,7 +596,7 @@ await withTaskExecutorPreference(specific) {
596596
group.addTask { computation() }
597597

598598
// child task executes on global concurrent executor
599-
group.addTask(executorPreference: globalConcurrentExecutor) {
599+
group.addTask(executorPreference: globalConcurrentExecutor) {
600600
async let compute = computation() // child task executes on the global concurrent executor
601601

602602
computation() // executed on the global concurrent executor
@@ -611,7 +611,7 @@ await withTaskExecutorPreference(specific) {
611611

612612
As with many new capabilities in libraries and languages, one may be tempted to use task executors to solve various problems.
613613

614-
We advise care when doing so with task executors, because while they do minimize the "hopping off" from executors and the associated context switching,
614+
We advise care when doing so with task executors, because while they do minimize the "hopping off" of executors and the associated context switching,
615615
this is also a behavior that may be entirely _undesirable_ in some situations. For example, over-hanging on the MainActor's executor is one of the main reasons
616616
earlier Swift versions moved to make `nonisolated` asynchronous functions always hop off their calling execution context; and this proposal brings back this behavior
617617
for specific executors.
@@ -631,7 +631,7 @@ The semantics explained in this proposal may at first seem tricky, however in re
631631

632632
It is worth discussing how user-control is retained with this proposal. Most notably, we believe this proposal follows Swift's core principle of progressive disclosure.
633633

634-
When developing an application at first one does not have to optimize for less context switches, however as applications grow performance analysis diagnoses context switching being a problem -- this proposal gives developers the tools to, selectively, in specific parts of a code-base introduce sticky task executor behavior.
634+
When developing an application at first one does not have to optimize for fewer context switches, however if as applications grow performance analysis diagnoses context switching being a problem this proposal gives developers the tools to, selectively, in specific parts of a code-base introduce sticky task executor behavior.
635635

636636
### Separating blocking code off the global shared pools
637637

@@ -657,13 +657,13 @@ public func callBulk() async -> Bytes {
657657
}
658658
```
659659

660-
This way we won't be blocking threads inside the shared pool, and not risking thread starving of the entire application.
660+
This way we won't be blocking threads inside the shared pool, and are not risking thread starving the entire application.
661661

662-
We can call `callRead` from inside `callBulk` and avoid un-necessary context switching as the same thread servicing the IO operation may be used for those asynchronous functions -- and no actual context switch may need to be performed when `callBulk` calls into `callRead` either.
662+
We can call `callRead` from inside `callBulk` and avoid unnecessary context switching as the same thread servicing the IO operation may be used for those asynchronous functions -- and no actual context switch may need to be performed when `callBulk` calls into `callRead` either.
663663

664-
For end-users of this library the API they don't need to worry about any of this, but the author of such library is in full control over where execution will happen -- be it using task executor preference, or custom actor executors.
664+
End-users of this library don't need to worry about any of this, but the author of such a library is in full control over where execution will happen -- be it using task executor preference, or custom actor executors.
665665

666-
This works also the other way around: when we're using a library and notice that it is doing blocking things and we'd rather separate it out onto a different executor. It may even have declared asynchronous methods -- but still is taking too long to yield the thread for some reason, causing issues to the shared pool.
666+
This also works the other way around, when a user of a library notices that it is doing blocking work which they would rather separate out onto a different executor. This is true even if the library has declared asynchronous methods but still is taking too long to yield the thread for some reason, causing issues to the shared pool.
667667

668668
```swift
669669
// SomeLibrary
@@ -683,9 +683,9 @@ func caller() async {
683683
}
684684
```
685685

686-
In other words, task executor preference gives control to developers at when and where care needs to be taken.
686+
In other words, task executor preference gives control to developers when and where care needs to be taken.
687687

688-
The default of hop-avoiding when a preference is set is also a good default because it optimizes for less context switching and can lead to better performance.
688+
The default of hop-avoiding when a preference is set has the benefit of optimizing for less context switching and can lead to better performance.
689689

690690
It is possible to effectively restore the default behavior as-if no task executor preference was present, by setting the preference to the `globalConcurrentExecutor` which is the executor used by default actors, tasks, and free async functions when no task executor preference is set:
691691

0 commit comments

Comments
 (0)