Skip to content

Commit 0723cc0

Browse files
authored
Merge pull request #74125 from apple/pick-docs
[Docs][Concurrency] Document custom executors in API docs a bit
2 parents e6cb5f4 + 92615f0 commit 0723cc0

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

stdlib/public/Concurrency/Actor.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ public typealias AnyActor = AnyObject & Sendable
3535
///
3636
/// The `Actor` protocol generalizes over all `actor` types. Actor types
3737
/// implicitly conform to this protocol.
38+
///
39+
/// ### Actors and SerialExecutors
40+
/// By default, actors execute tasks on a shared global concurrency thread pool.
41+
/// This pool is shared by all default actors and tasks, unless an actor or task
42+
/// specified a more specific executor requirement.
43+
///
44+
/// It is possible to configure an actor to use a specific ``SerialExecutor``,
45+
/// as well as impact the scheduling of default tasks and actors by using
46+
/// a ``TaskExecutor``.
47+
///
48+
/// - SeeAlso: ``SerialExecutor``
49+
/// - SeeAlso: ``TaskExecutor``
3850
@available(SwiftStdlib 5.1, *)
3951
public protocol Actor: AnyObject, Sendable {
4052

@@ -50,6 +62,9 @@ public protocol Actor: AnyObject, Sendable {
5062
/// eliminated, and rearranged with other work, and they may even
5163
/// be introduced when not strictly required. Visible side effects
5264
/// are therefore strongly discouraged within this property.
65+
///
66+
/// - SeeAlso: ``SerialExecutor``
67+
/// - SeeAlso: ``TaskExecutor``
5368
nonisolated var unownedExecutor: UnownedSerialExecutor { get }
5469
}
5570

stdlib/public/Concurrency/Executor.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,76 @@ public protocol Executor: AnyObject, Sendable {
3838
}
3939

4040
/// A service that executes jobs.
41+
///
42+
/// ### Custom Actor Executors
43+
/// By default, all actor types execute tasks on a shared global concurrent pool.
44+
/// The global pool does not guarantee any thread (or dispatch queue) affinity,
45+
/// so actors are free to use different threads as they execute tasks.
46+
///
47+
/// > The runtime may perform various optimizations to minimize un-necessary
48+
/// > thread switching.
49+
///
50+
/// Sometimes it is important to be able to customize the execution behavior
51+
/// of an actor. For example, when an actor is known to perform heavy blocking
52+
/// operations (such as IO), and we would like to keep this work *off* the global
53+
/// shared pool, as blocking it may prevent other actors from being responsive.
54+
///
55+
/// You can implement a custom executor, by conforming a type to the
56+
/// ``SerialExecutor`` protocol, and implementing the ``enqueue(_:)`` method.
57+
///
58+
/// Once implemented, you can configure an actor to use such executor by
59+
/// implementing the actor's ``Actor/unownedExecutor`` computed property.
60+
/// For example, you could accept an executor in the actor's initializer,
61+
/// store it as a variable (in order to retain it for the duration of the
62+
/// actor's lifetime), and return it from the `unownedExecutor` computed
63+
/// property like this:
64+
///
65+
/// ```
66+
/// actor MyActor {
67+
/// let myExecutor: MyExecutor
68+
///
69+
/// // accepts an executor to run this actor on.
70+
/// init(executor: MyExecutor) {
71+
/// self.myExecutor = executor
72+
/// }
73+
///
74+
/// nonisolated var unownedExecutor: UnownedSerialExecutor {
75+
/// self.myExecutor.asUnownedSerialExecutor()
76+
/// }
77+
/// }
78+
/// ```
79+
///
80+
/// It is also possible to use a form of shared executor, either created as a
81+
/// global or static property, which you can then re-use for every MyActor
82+
/// instance:
83+
///
84+
/// ```
85+
/// actor MyActor {
86+
/// // Serial executor reused by *all* instances of MyActor!
87+
/// static let sharedMyActorsExecutor = MyExecutor() // implements SerialExecutor
88+
///
89+
///
90+
/// nonisolated var unownedExecutor: UnownedSerialExecutor {
91+
/// Self.sharedMyActorsExecutor.asUnownedSerialExecutor()
92+
/// }
93+
/// }
94+
/// ```
95+
///
96+
/// In the example above, *all* "MyActor" instances would be using the same
97+
/// serial executor, which would result in only one of such actors ever being
98+
/// run at the same time. This may be useful if some of your code has some
99+
/// "specific thread" requirement when interoperating with non-Swift runtimes
100+
/// for example.
101+
///
102+
/// Since the ``UnownedSerialExecutor`` returned by the `unownedExecutor`
103+
/// property *does not* retain the executor, you must make sure the lifetime of
104+
/// it extends beyond the lifetime of any actor or task using it, as otherwise
105+
/// it may attempt to enqueue work on a released executor object, causing a crash.
106+
/// The executor returned by unownedExecutor *must* always be the same object,
107+
/// and returning different executors can lead to unexpected behavior.
108+
///
109+
/// Alternatively, you can also use existing serial executor implementations,
110+
/// such as Dispatch's `DispatchSerialQueue` or others.
41111
@available(SwiftStdlib 5.1, *)
42112
public protocol SerialExecutor: Executor {
43113
// This requirement is repeated here as a non-override so that we

stdlib/public/Concurrency/Task.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,11 @@ import Swift
127127
/// await Actor().start()
128128
/// ```
129129
///
130-
/// Note that there is nothing, other than the Task's use of `self` retaining the actor,
131-
/// And that the start method immediately returns, without waiting for the unstructured `Task` to finish.
132-
/// So once the task is completed and its closure is destroyed, the strong reference to the "self" of the actor is also released allowing the actor to deinitialize as expected.
130+
/// Note that the actor is only retained by the start() method's use of `self`,
131+
/// and that the start method immediately returns, without waiting for the
132+
/// unstructured `Task` to finish. Once the task is completed and its closure is
133+
/// destroyed, the strong reference to the actor is also released allowing the
134+
/// actor to deinitialize as expected.
133135
///
134136
/// Therefore, the above call will consistently result in the following output:
135137
///

0 commit comments

Comments
 (0)