Skip to content

[Docs][Concurrency] Document custom executors in API docs a bit #73634

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

Merged
merged 2 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions stdlib/public/Concurrency/Actor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ public typealias AnyActor = AnyObject & Sendable
///
/// The `Actor` protocol generalizes over all `actor` types. Actor types
/// implicitly conform to this protocol.
///
/// ### Actors and SerialExecutors
/// By default, actors execute tasks on a shared global concurrency thread pool.
/// This pool is shared by all default actors and tasks, unless an actor or task
/// specified a more specific executor requirement.
///
/// It is possible to configure an actor to use a specific ``SerialExecutor``,
/// as well as impact the scheduling of default tasks and actors by using
/// a ``TaskExecutor``.
///
/// - SeeAlso: ``SerialExecutor``
/// - SeeAlso: ``TaskExecutor``
@available(SwiftStdlib 5.1, *)
public protocol Actor: AnyObject, Sendable {

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

Expand Down
70 changes: 70 additions & 0 deletions stdlib/public/Concurrency/Executor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,76 @@ public protocol Executor: AnyObject, Sendable {
}

/// A service that executes jobs.
///
/// ### Custom Actor Executors
/// By default, all actor types execute tasks on a shared global concurrent pool.
/// The global pool does not guarantee any thread (or dispatch queue) affinity,
/// so actors are free to use different threads as they execute tasks.
///
/// > The runtime may perform various optimizations to minimize un-necessary
/// > thread switching.
///
/// Sometimes it is important to be able to customize the execution behavior
/// of an actor. For example, when an actor is known to perform heavy blocking
/// operations (such as IO), and we would like to keep this work *off* the global
/// shared pool, as blocking it may prevent other actors from being responsive.
///
/// You can implement a custom executor, by conforming a type to the
/// ``SerialExecutor`` protocol, and implementing the ``enqueue(_:)`` method.
///
/// Once implemented, you can configure an actor to use such executor by
/// implementing the actor's ``Actor/unownedExecutor`` computed property.
/// For example, you could accept an executor in the actor's initializer,
/// store it as a variable (in order to retain it for the duration of the
/// actor's lifetime), and return it from the `unownedExecutor` computed
/// property like this:
///
/// ```
/// actor MyActor {
/// let myExecutor: MyExecutor
///
/// // accepts an executor to run this actor on.
/// init(executor: MyExecutor) {
/// self.myExecutor = executor
/// }
///
/// nonisolated var unownedExecutor: UnownedSerialExecutor {
/// self.myExecutor.asUnownedSerialExecutor()
/// }
/// }
/// ```
///
/// It is also possible to use a form of shared executor, either created as a
/// global or static property, which you can then re-use for every MyActor
/// instance:
///
/// ```
/// actor MyActor {
/// // Serial executor reused by *all* instances of MyActor!
/// static let sharedMyActorsExecutor = MyExecutor() // implements SerialExecutor
///
///
/// nonisolated var unownedExecutor: UnownedSerialExecutor {
/// Self.sharedMyActorsExecutor.asUnownedSerialExecutor()
/// }
/// }
/// ```
///
/// In the example above, *all* "MyActor" instances would be using the same
/// serial executor, which would result in only one of such actors ever being
/// run at the same time. This may be useful if some of your code has some
/// "specific thread" requirement when interoperating with non-Swift runtimes
/// for example.
///
/// Since the ``UnownedSerialExecutor`` returned by the `unownedExecutor`
/// property *does not* retain the executor, you must make sure the lifetime of
/// it extends beyond the lifetime of any actor or task using it, as otherwise
/// it may attempt to enqueue work on a released executor object, causing a crash.
/// The executor returned by unownedExecutor *must* always be the same object,
/// and returning different executors can lead to unexpected behavior.
///
/// Alternatively, you can also use existing serial executor implementations,
/// such as Dispatch's `DispatchSerialQueue` or others.
@available(SwiftStdlib 5.1, *)
public protocol SerialExecutor: Executor {
// This requirement is repeated here as a non-override so that we
Expand Down
8 changes: 5 additions & 3 deletions stdlib/public/Concurrency/Task.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,11 @@ import Swift
/// await Actor().start()
/// ```
///
/// Note that there is nothing, other than the Task's use of `self` retaining the actor,
/// And that the start method immediately returns, without waiting for the unstructured `Task` to finish.
/// 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.
/// Note that the actor is only retained by the start() method's use of `self`,
/// and that the start method immediately returns, without waiting for the
/// unstructured `Task` to finish. Once the task is completed and its closure is
/// destroyed, the strong reference to the actor is also released allowing the
/// actor to deinitialize as expected.
///
/// Therefore, the above call will consistently result in the following output:
///
Expand Down