From 5f24fafc31b1bdffd913095ec30d6d78e4c6094b Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Mon, 9 Dec 2024 12:07:12 +0000 Subject: [PATCH 01/10] Initial public draft of custom executors proposal. --- .../nnnn-custom-main-and-global-executors.md | 574 ++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 proposals/nnnn-custom-main-and-global-executors.md diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md new file mode 100644 index 0000000000..02b74f5573 --- /dev/null +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -0,0 +1,574 @@ +# Custom Main and Global Executors + +* Proposal: [SE-NNNN](NNNN-custom-main-and-global-executors.md) +* Authors: [Alastair Houghton](https://github.com/al45tair), [Konrad + Malawski](https://github.com/ktoso), [Evan Wilde](https://github.com/etcwilde) +* Review Manager: TBD +* Status: **Pitch, Awaiting Implementation** +* Implementation: TBA +* Review: + +## Introduction + +Currently the built-in executor implementations are provided directly +by the Swift Concurrency runtime, and are built on top of Dispatch. +While developers can currently provide custom executors, it is not +possible to override the main executor (which corresponds to the main +thread/main actor) or the global default executor; this proposal is +intended to allow such an override, while also laying the groundwork +for the runtime itself to implement its default executors in Swift. + +## Motivation + +The decision to provide fixed built-in executor implementations works +well on Darwin, where it reflects the fact that the OS uses Dispatch +as its high level system-wide concurrency primitive and where Dispatch +is integrated into the standard system-wide run loop implementation in +Core Foundation. + +Other platforms, however, often use different concurrency mechanisms +and run loops; they may not even have a standard system-wide run loop, +or indeed a standard system-wide high level concurrency system, +instead relying on third-party libraries like `libuv`, `libevent` or +`libev`, or on GUI frameworks like `Qt` or `MFC`. A further +complication is that in some situations there are options that, while +supported by the underlying operating system, are prohibited by the +execution environment (for instance, `io_uring` is commonly disabled +in container and server environments because it has been a source of +security issues), which means that some programs may wish to be able +to select from a number of choices depending on configuration or +program arguments. + +Additionally, in embedded applications, particularly on bare metal or +without a fully featured RTOS, it is likely that using Swift +Concurrency will require a fully custom executor; if there is a +separation between platform code (for instance a Board Support +Package or BSP) and application code, it is very likely that this +would be provided by the platform code rather than the application. + +Finally, the existing default executor implementations are written in +C++, not Swift. We would like to use Swift for the default +implementations in the runtime, so whatever interface we define here +needs to be usable for that. + +## Current Swift support for Executors + +It is useful to provide a brief overview of what we already have in +terms of executor, job and task provision on the Swift side. The +definitions presented below are simplified and in some cases +additional comments have been provided where there were none in the +code. + +**N.B. This section is not design work; it is a statement of the +existing interfaces, to aid in discussion.*** + +### Existing `Executor` types + +There are already some Swift `Executor` protocols defined by the +Concurrency runtime, namely: + +```swift +public protocol Executor: AnyObject, Sendable { + + /// Enqueue a job on this executor + func enqueue(_ job: UnownedJob) + + /// Enqueue a job on this executor + func enqueue(_ job: consuming ExecutorJob) + +} + +public protocol SerialExecutor: Executor { + + /// Convert this executor value to the optimized form of borrowed + /// executor references. + func asUnownedSerialExecutor() -> UnownedSerialExecutor + + /// For executors with "complex equality semantics", this function + /// is called by the runtime when comparing two executor instances. + /// + /// - Parameter other: the executor to compare with. + /// - Returns: `true`, if `self` and the `other` executor actually are + /// mutually exclusive and it is safe–from a concurrency + /// perspective–to execute code assuming one on the other. + func isSameExclusiveExecutionContext(other: Self) -> Bool + + /// Last resort isolation check, called by the runtime when it is + /// trying to check that we are running on a particular executor and + /// it is unable to prove serial equivalence between this executor and + /// the current executor. + /// + /// A default implementation is provided that unconditionally crashes the + /// program, and prevents calling code from proceeding with potentially + /// not thread-safe execution. + func checkIsolated() + +} + +public protocol TaskExecutor: Executor { + + /// Convert this executor value to the optimized form of borrowed + /// executor references. + func asUnownedTaskExecutor() -> UnownedTaskExecutor + +} +``` + +The various `Unowned` types are wrappers that allow for manipulation +of unowned references to their counterparts. `Unowned` executor types +do not conform to their respective `Executor` protocols. + +### Jobs and Tasks + +Users of Concurrency are probably familiar with `Task`s, but +`ExecutorJob` (previously known as `Job`) is likely less familiar. + +Executors schedule jobs (`ExecutorJob`s), _not_ `Task`s. `Task` +represents a unit of asynchronous work that a client of Swift +Concurrency wishes to execute; it is backed internally by a job +object, which on the Swift side means an `ExecutorJob`. Note that +there are `ExecutorJob`s that do not represent Swift `Task`s (for +instance, running an isolated `deinit` requires a job). + +`ExecutorJob` has the following interface: + +```swift +@frozen +public struct ExecutorJob: Sendable, ~Copyable { + /// Convert from an `UnownedJob` reference + public init(_ job: UnownedJob) + + /// Get the priority of this job. + public var priority: JobPriority { get } + + /// Get a description of this job. We don't conform to + /// `CustomStringConvertible` because this is a move-only type. + public var description: String { get } + + /// Run this job on the passed-in executor. + /// + /// - Parameter executor: the executor this job will be semantically running on. + consuming public func runSynchronously(on executor: UnownedSerialExecutor) + + /// Run this job on the passed-in executor. + /// + /// - Parameter executor: the executor this job will be semantically running on. + consuming public func runSynchronously(on executor: UnownedTaskExecutor) + + /// Run this job isolated to the passed-in serial executor, while executing + /// it on the specified task executor. + /// + /// - Parameter serialExecutor: the executor this job will be semantically running on. + /// - Parameter taskExecutor: the task executor this job will be run on. + /// + /// - SeeAlso: ``runSynchronously(on:)`` + consuming public func runSynchronously( + isolatedTo serialExecutor: UnownedSerialExecutor, + taskExecutor: UnownedTaskExecutor + ) +} +``` + +where `JobPriority` is: + +```swift +@frozen +public struct JobPriority: Sendable, Equatable, Comparable { + public typealias RawValue = UInt8 + + /// The raw priority value. + public var rawValue: RawValue +} +``` + +### `async` `main` entry point + +Programs that use Swift Concurrency start from an `async` version of +the standard Swift `main` function: + +```swift +@main +struct MyApp { + static func main() async { + ... + print("Before the first await") + await foo() + print("After the first await") + ... + } +} +``` + +As with all `async` functions, this is transformed by the compiler +into a set of partial functions, each of which corresponds to an +"async basic block" (that is, a block of code that is ended by an +`await` or by returning from the function). The main entry point is +however a little special, in that it is additionally responsible for +transitioning from synchronous to asynchronous execution, so the +compiler inserts some extra code into the first partial function, +something like the following pseudo-code: + +```swift +func _main1() { + ... + print("Before the first await") + MainActor.unownedExecutor.enqueue(_main2) + _swift_task_asyncMainDrainQueue() +} + +func _main2() { + foo() + print("After the first await") + ... +} +``` + +`_swift_task_asyncMainDrainQueue()` is part of the Swift ABI on +Darwin, and on Darwin boils down to something like (simplified): + +```c +void _swift_task_asyncMainDrainQueue() { + if (CFRunLoopRun) { + CFRunLoopRun(); + exit(0); + } + dispatch_main(); +} +``` + +which works because on Darwin the main executor enqueues tasks onto +the main dispatch queue, which is serviced by Core Foundation's run +loop or by Dispatch if Core Foundation is for some reason not present. + +The important point to note here is that before the first `await`, the +code is running in the normal, synchronous style; until the first +enqueued task, which is _normally_ the one added by the compiler at +the end of the first part of the main function, you can safely alter +the executor and perform other Concurrency set-up. + +## Proposed solution + +We propose adding a new protocol to represent an Executor that is +backed by some kind of run loop: + +```swift +protocol RunLoopExecutor: Executor { + /// Run the executor's run loop. + /// + /// This method will synchronously block the calling thread. Nested calls + /// to `run()` are permitted, however it is not permitted to call `run()` + /// on a single executor instance from more than one thread. + func run() throws + + /// Signal to the runloop to stop running and return. + /// + /// This method may be called from the same thread that is in the `run()` + /// method, or from some other thread. It will not wait for the run loop + /// to stop; calling this method simply signals that the run loop *should*, + /// as soon as is practicable, stop the innermost `run()` invocation + /// and make that `run()` invocation return. + func stop() +} +``` + +We will also add a protocol for `RunLoopExecutor`s that are also +`SerialExecutors`: + +```swift +protocol SerialRunLoopExecutor: RunLoopExecutor & SerialExecutor { +} +``` + +We will then expose properties on `MainActor` and `Task` to allow +users to query or set the executors: + +```swift +extension MainActor { + /// The main executor, which is started implicitly by the `async main` + /// entry point and owns the "main" thread. + /// + /// Attempting to set this after the first `enqueue` on the main + /// executor is a fatal error. + public static var executor: any SerialRunLoopExecutor { get set } +} + +extension Task { + /// The default or global executor, which is the default place in which + /// we run tasks. + /// + /// Attempting to set this after the first `enqueue` on the global + /// executor is a fatal error. + public static var defaultExecutor: any Executor { get set } +} +``` + +The platform-specific default implementations of these two executors will also be +exposed with the names below: + +``` swift +/// The default main executor implementation for the current platform. +public struct PlatformMainExecutor: SerialRunLoopExecutor { + ... +} + +/// The default global executor implementation for the current platform. +public struct PlatformDefaultExecutor: Executor { + ... +} +``` + +We will also need to expose the executor storage fields on +`ExecutorJob`, so that they are accessible to Swift implementations of +the `Executor` protocols: + +```swift +struct ExecutorJob { + ... + + /// Storage reserved for the scheduler (exactly two UInts in size) + var schedulerPrivate: some Collection + + /// What kind of job this is + var kind: ExecutorJobKind + ... +} + +/// Kinds of schedulable jobs. +@frozen +public struct ExecutorJobKind: Sendable { + public typealias RawValue = UInt8 + + /// The raw job kind value. + public var rawValue: RawValue + + /// A task + public static let task = RawValue(0) + + // Job kinds >= 192 are private to the implementation + public static let firstReserved = RawValue(192) +} +``` + +Finally, jobs of type `JobKind.task` have the ability to allocate task +memory, using a stack disciplined allocator; this memory is +automatically released when the task itself is released. We will +expose some new functions on `ExecutorJob` to allow access to this +facility: + +```swift +extension ExecutorJob { + + /// Allocate a specified number of bytes of uninitialized memory. + public func allocate(capacity: Int) -> UnsafeMutableRawBufferPointer? + + /// Allocate uninitialized memory for a single instance of type `T`. + public func allocate(as: T.Type) -> UnsafeMutablePointer? + + /// Allocate uninitialized memory for the specified number of + /// instances of type `T`. + public func allocate(capacity: Int, as: T.Type) + -> UnsafeMutableBufferPointer? + + /// Deallocate previously allocated memory. Note that the task + /// allocator is stack disciplined, so if you deallocate a block of + /// memory, all memory allocated after that block is also deallocated. + public func deallocate(_ buffer: UnsafeMutableRawBufferPointer?) + + /// Deallocate previously allocated memory. Note that the task + /// allocator is stack disciplined, so if you deallocate a block of + /// memory, all memory allocated after that block is also deallocated. + public func deallocate(_ pointer: UnsafeMutablePointer?) + + /// Deallocate previously allocated memory. Note that the task + /// allocator is stack disciplined, so if you deallocate a block of + /// memory, all memory allocated after that block is also deallocated. + public func deallocate(_ buffer: UnsafeMutableBufferPointer?) + +} +``` + +Calling these on a job not of kind `JobKind.task` is a fatal error. + +### Embedded Swift + +For Embedded Swift we will provide default implementations of the main +and default executor that call C functions; this means that Embedded +Swift users can choose to implement those C functions to override the +default behaviour. This is desirable because Swift is not designed to +support externally defined Swift functions, types or methods in the +same way that C is. + +We will also add a compile-time option to the Concurrency runtime to +allow users of Embedded Swift to disable the ability to dynamically +set the executors, as this is an option that may not be necessary in +that case. When this option is enabled, the `executor` and +`defaultExecutor` properties will be as follows (rather than using +existentials): + +```swift +extension MainActor { + /// The main executor, which is started implicitly by the `async main` + /// entry point and owns the "main" thread. + public static var executor: PlatformMainExecutor { get } +} + +extension Task { + /// The default or global executor, which is the default place in which + /// we run tasks. + public static var defaultExecutor: PlatformDefaultExecutor { get } +} +``` + +If this option is enabled, an Embedded Swift program that wishes to +customize executor behaviour will have to use the C API. + +## Detailed design + +### `async` main code generation + +The compiler's code generation for `async` main functions will change +to something like + +```swift +func _main1() { + ... + print("Before the first await") + MainActor.executor.enqueue(_main2) + MainActor.executor.run() +} + +func _main2() { + foo() + print("After the first await") + ... +} +``` + +## Source compatibility + +There should be no source compatibility concerns, as this proposal is +purely additive from a source code perspective. + +## ABI compatibility + +On Darwin we have a number of functions in the runtime that form part +of the ABI and we will need those to continue to function as expected. +This includes `_swift_task_asyncMainDrainQueue()` as well as a number +of hook functions that are used by Swift NIO. + +The new `async` `main` entry point code will only work with a newer +runtime. + +## Implications on adoption + +Software wishing to adopt these new features will need to target a +Concurrency runtime version that has support for them. On Darwin, +software targeting a minimum system version that is too old to +guarantee the presence of the new runtime code in the OS will cause +the compiler to generate the old-style `main` entry point code. +We do not intend to support back-deployment of these features. + +## Future directions + +We are contemplating the possibility of providing pseudo-blocking +capabilities, perhaps only for code on the main actor, which is why we +think we want `run()` and `stop()` on `RunLoopExecutor`. + +## Alternatives considered + +### `typealias` in entry point struct + +The idea here would be to have the `@main` `struct` declare the +executor type that it wants. + +This is straightforward for users, _but_ doesn't work for top-level +code, and also doesn't allow the user to change executor based on +configuration (e.g. "use the `epoll()` based executor, not the +`io_uring` based executor"), as it's fixed at compile time. + +### Adding a `createExecutor()` method to the entry point struct + +This is nice because it prevents the user from trying to change +executor at a point after that is no longer possible. + +The downside is that it isn't really suitable for top-level code (we'd +need to have a magic function name for that, which is less pleasant). + +### Allowing `RunLoopExecutor.run()` to return immediately + +We discussed allowing `RunLoopExecutor.run()` to return immediately, +as it might if we were using that protocol as a way to explicitly +_start_ an executor that didn't actually have a run loop. + +While there might conceivably _be_ executors that would want such a +method, they are not really "run loop executors", in that they are not +running a central loop. Since the purpose of `RunLoopExecutor` is to +deal with executors that _do_ need a central loop, it seems that +executors that want a non-blocking `run` method could instead be a +different type. + +### Not having `RunLoopExecutor` + +It's possible to argue that we don't need `RunLoopExecutor`, that the +platform knows how to start and run the default main executor, and +that anyone replacing the main executor will likewise know how they're +going to start it. + +However, it turns out that it is useful to be able to use the +`RunLoopExecutor` protocol to make nested `run()` invocations, which +will allow us to block on asynchronous work from synchronous code +(the details of this are left for a future SE proposal). + +### `defaultExecutor` on `ExecutorJob` rather than `Task` + +This makes some sense from the perspective of implementors of +executors, particularly given that there genuinely are `ExecutorJob`s +that do not correspond to `Task`s, but normal Swift users never touch +`ExecutorJob`. + +Further, `ExecutorJob` is a `struct`, not a protocol, and so it isn't +obvious from the Swift side of things that there is any relationship +between `Task` and `ExecutorJob`. Putting the property on +`ExecutorJob` would therefore make it difficult to discover. + +### Altering the way the compiler starts `async` main + +The possibility was raised, in the context of Embedded Swift in +particular, that we could change the compiler such that the platform +exposes a function + +```swift +func _startMain() { + // Set-up the execution environment + ... + + // Start main() + Task { main() } + + // Enter the main loop here + ... +} +``` + +The main downside of this is that this would be a source compatibility +break for places where Swift Concurrency already runs, because some +existing code already knows that it is not really asynchronous until +the first `await` in the main entry point. + +### Building support for clocks into `Executor` + +While the existing C interfaces within Concurrency do associate clocks +with executors, there is in fact no real need to do this, and it's +only that way internally because Dispatch happens to handle timers and +it was easy to write the implementation this way. + +In reality, timer-based scheduling can be handled through some +appropriate platform-specific mechanism, and when the relevant timer +fires the task that was scheduled for a specific time can be enqueued +on an appropriate executor using the `enqueue()` method. + +## Acknowledgments + +Thanks to Cory Benfield, Franz Busch, David Greenaway, Rokhini Prabhu, +Rauhul Varma, Johannes Weiss, and Matt Wright for their input on this +proposal. From d176b251f1cc78111d6f78045c1fd3844ac035db Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Mon, 20 Jan 2025 16:21:43 +0000 Subject: [PATCH 02/10] Updates following pitch comments. Use `any TaskExecutor` instead of `any Executor` for `Task.defaultExecutor`. Rename `ExecutorJobKind` to `ExecutorJob.Kind`. Add `EventableExecutor`; replace `SerialRunLoopExecutor` with `MainExecutor`, then make `MainActor` and `PlatformMainExecutor` use the new protocol. --- .../nnnn-custom-main-and-global-executors.md | 99 ++++++++++++++----- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 02b74f5573..12da821091 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -271,11 +271,11 @@ protocol RunLoopExecutor: Executor { } ``` -We will also add a protocol for `RunLoopExecutor`s that are also -`SerialExecutors`: +We will also add a protocol for the main actor's executor (see later +for details of `EventableExecutor` and why it exists): ```swift -protocol SerialRunLoopExecutor: RunLoopExecutor & SerialExecutor { +protocol MainExecutor: RunLoopExecutor & SerialExecutor & EventableExecutor { } ``` @@ -289,7 +289,7 @@ extension MainActor { /// /// Attempting to set this after the first `enqueue` on the main /// executor is a fatal error. - public static var executor: any SerialRunLoopExecutor { get set } + public static var executor: any MainExecutor { get set } } extension Task { @@ -298,7 +298,7 @@ extension Task { /// /// Attempting to set this after the first `enqueue` on the global /// executor is a fatal error. - public static var defaultExecutor: any Executor { get set } + public static var defaultExecutor: any TaskExecutor { get set } } ``` @@ -307,12 +307,12 @@ exposed with the names below: ``` swift /// The default main executor implementation for the current platform. -public struct PlatformMainExecutor: SerialRunLoopExecutor { +public struct PlatformMainExecutor: MainExecutor { ... } /// The default global executor implementation for the current platform. -public struct PlatformDefaultExecutor: Executor { +public struct PlatformDefaultExecutor: TaskExecutor { ... } ``` @@ -325,27 +325,27 @@ the `Executor` protocols: struct ExecutorJob { ... - /// Storage reserved for the scheduler (exactly two UInts in size) - var schedulerPrivate: some Collection + /// Storage reserved for the executor + var executorPrivate: (UInt, UInt) - /// What kind of job this is - var kind: ExecutorJobKind - ... -} + /// Kinds of schedulable jobs. + @frozen + public struct Kind: Sendable { + public typealias RawValue = UInt8 -/// Kinds of schedulable jobs. -@frozen -public struct ExecutorJobKind: Sendable { - public typealias RawValue = UInt8 + /// The raw job kind value. + public var rawValue: RawValue - /// The raw job kind value. - public var rawValue: RawValue + /// A task + public static let task = RawValue(0) - /// A task - public static let task = RawValue(0) + // Job kinds >= 192 are private to the implementation + public static let firstReserved = RawValue(192) + } - // Job kinds >= 192 are private to the implementation - public static let firstReserved = RawValue(192) + /// What kind of job this is + var kind: Kind + ... } ``` @@ -422,6 +422,59 @@ extension Task { If this option is enabled, an Embedded Swift program that wishes to customize executor behaviour will have to use the C API. +### Coalesced Event Interface + +We would like custom main executors to be able to integrate with other +libraries, without tying the implementation to a specific library; in +practice, this means that the executor will need to be able to trigger +processing from some external event. + +```swift +protocol EventableExecutor { + + /// An opaque, executor-dependent type used to represent an event. + associatedtype Event + + /// Register a new event with a given handler. + /// + /// Notifying the executor of the event will cause the executor to + /// execute the handler, however the executor is free to coalesce multiple + /// event notifications, and is also free to execute the handler at a time + /// of its choosing. + /// + /// Parameters + /// + /// - handler: The handler to call when the event fires. + /// + /// Returns a new opaque `Event`. + public func registerEvent(handler: @escaping () -> ()) -> Event + + /// Deregister the given event. + /// + /// After this function returns, there will be no further executions of the + /// handler for the given event. + public func deregister(event: Event) + + /// Notify the executor of an event. + /// + /// This will trigger, at some future point, the execution of the associated + /// event handler. Prior to that time, multiple calls to `notify` may be + /// coalesced and result in a single invocation of the event handler. + public func notify(event: Event) + +} +``` + +Our expectation is that a library that wishes to integrate with the +main executor will register an event with the main executor, and can +then notify the main executor of that event, which will trigger the +executor to run the associated handler at an appropriate time. + +The point of this interface is that a library can rely on the executor +to coalesce these events, such that the handler will be triggered once +for a potentially long series of `MainActor.executor.notify(event:)` +invocations. + ## Detailed design ### `async` main code generation From 9147b86ea81ced1b46874df227f893682514fac0 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Tue, 21 Jan 2025 11:26:44 +0000 Subject: [PATCH 03/10] Add `ExecutorJob.allocator` and move allocation methods there. The previous iteration would have required us to make calling the allocation methods a fatal error, and would have meant executors had to check explicitly for the `.task` job kind before using them. Instead, if we add a new `LocalAllocator` type, we can have an `allocator` property that is `nil` if the job doesn't support allocation, and a valid `LocalAllocator` otherwise. --- .../nnnn-custom-main-and-global-executors.md | 96 +++++++++++++------ 1 file changed, 68 insertions(+), 28 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 12da821091..3a996bb9c6 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -326,7 +326,7 @@ struct ExecutorJob { ... /// Storage reserved for the executor - var executorPrivate: (UInt, UInt) + public var executorPrivate: (UInt, UInt) /// Kinds of schedulable jobs. @frozen @@ -344,50 +344,90 @@ struct ExecutorJob { } /// What kind of job this is - var kind: Kind + public var kind: Kind ... } ``` -Finally, jobs of type `JobKind.task` have the ability to allocate task -memory, using a stack disciplined allocator; this memory is -automatically released when the task itself is released. We will -expose some new functions on `ExecutorJob` to allow access to this -facility: +Finally, jobs of type `ExecutorJob.Kind.task` have the ability to +allocate task memory, using a stack disciplined allocator; this memory +is automatically released when the task itself is released. + +Rather than require users to test the job kind to discover this, which +would mean that they would not be able to use allocation on new job +types we might add in future, or on other existing job types that +might gain allocation support, it seems better to provide an interface +that will allow users to conditionally acquire an allocator. We are +therefore proposing that `ExecutorJob` gain ```swift extension ExecutorJob { - /// Allocate a specified number of bytes of uninitialized memory. - public func allocate(capacity: Int) -> UnsafeMutableRawBufferPointer? + /// Obtain a stack-disciplined job-local allocator. + /// + /// If the job does not support allocation, this property will be + /// `nil`. + public var allocator: LocalAllocator? { get } + + /// A job-local stack-disciplined allocator. + /// + /// This can be used to allocate additional data required by an + /// executor implementation; memory allocated in this manner will + /// be released automatically when the job is disposed of by the + /// runtime. + /// + /// N.B. Because this allocator is stack disciplined, explicitly + /// deallocating memory will also deallocate all memory allocated + /// after the block being deallocated. + struct LocalAllocator { - /// Allocate uninitialized memory for a single instance of type `T`. - public func allocate(as: T.Type) -> UnsafeMutablePointer? + /// Allocate a specified number of bytes of uninitialized memory. + public func allocate(capacity: Int) -> UnsafeMutableRawBufferPointer? - /// Allocate uninitialized memory for the specified number of - /// instances of type `T`. - public func allocate(capacity: Int, as: T.Type) - -> UnsafeMutableBufferPointer? + /// Allocate uninitialized memory for a single instance of type `T`. + public func allocate(as: T.Type) -> UnsafeMutablePointer? - /// Deallocate previously allocated memory. Note that the task - /// allocator is stack disciplined, so if you deallocate a block of - /// memory, all memory allocated after that block is also deallocated. - public func deallocate(_ buffer: UnsafeMutableRawBufferPointer?) + /// Allocate uninitialized memory for the specified number of + /// instances of type `T`. + public func allocate(capacity: Int, as: T.Type) + -> UnsafeMutableBufferPointer? - /// Deallocate previously allocated memory. Note that the task - /// allocator is stack disciplined, so if you deallocate a block of - /// memory, all memory allocated after that block is also deallocated. - public func deallocate(_ pointer: UnsafeMutablePointer?) + /// Deallocate previously allocated memory. Note that the task + /// allocator is stack disciplined, so if you deallocate a block of + /// memory, all memory allocated after that block is also deallocated. + public func deallocate(_ buffer: UnsafeMutableRawBufferPointer?) - /// Deallocate previously allocated memory. Note that the task - /// allocator is stack disciplined, so if you deallocate a block of - /// memory, all memory allocated after that block is also deallocated. - public func deallocate(_ buffer: UnsafeMutableBufferPointer?) + /// Deallocate previously allocated memory. Note that the task + /// allocator is stack disciplined, so if you deallocate a block of + /// memory, all memory allocated after that block is also deallocated. + public func deallocate(_ pointer: UnsafeMutablePointer?) + + /// Deallocate previously allocated memory. Note that the task + /// allocator is stack disciplined, so if you deallocate a block of + /// memory, all memory allocated after that block is also deallocated. + public func deallocate(_ buffer: UnsafeMutableBufferPointer?) + + } } ``` -Calling these on a job not of kind `JobKind.task` is a fatal error. +In the current implementation, `allocator` will be `nil` for jobs +other than those of type `ExecutorJob.Kind.task`. This means that you +can write code like + +```swift +if let chunk = job.allocator?.allocate(capacity: 1024) { + + // Job supports allocation and `chunk` is a 1,024-byte buffer + ... + +} else { + + // Job does not support allocation + +} +``` ### Embedded Swift From 8e0fe92146ef8cb70d6277f81ea079634db5b140 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Tue, 11 Mar 2025 16:21:54 +0000 Subject: [PATCH 04/10] Updated to match current implementation. Since the previous pitch, I've added `Clock`-based `enqueue` methods and made a few other changes, notably to the way you actually set custome executors for your program. --- .../nnnn-custom-main-and-global-executors.md | 381 ++++++++++++++---- 1 file changed, 308 insertions(+), 73 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 3a996bb9c6..90e8c6a4f1 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -6,7 +6,7 @@ * Review Manager: TBD * Status: **Pitch, Awaiting Implementation** * Implementation: TBA -* Review: +* Review: ([original pitch](https://forums.swift.org/t/pitch-custom-main-and-global-executors/77247) ([pitch])(https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437) ## Introduction @@ -210,13 +210,18 @@ something like the following pseudo-code: ```swift func _main1() { - ... - print("Before the first await") - MainActor.unownedExecutor.enqueue(_main2) + let task = Task(_main2) + task.runSynchronously() + MainActor.unownedExecutor.enqueue(_main3) _swift_task_asyncMainDrainQueue() } func _main2() { + ... + print("Before the first await") +} + +func _main3() { foo() print("After the first await") ... @@ -252,21 +257,38 @@ We propose adding a new protocol to represent an Executor that is backed by some kind of run loop: ```swift -protocol RunLoopExecutor: Executor { +/// An executor that is backed by some kind of run loop. +/// +/// The idea here is that some executors may work by running a loop +/// that processes events of some sort; we want a way to enter that loop, +/// and we would also like a way to trigger the loop to exit. +public protocol RunLoopExecutor: Executor { /// Run the executor's run loop. /// - /// This method will synchronously block the calling thread. Nested calls - /// to `run()` are permitted, however it is not permitted to call `run()` - /// on a single executor instance from more than one thread. + /// This method will synchronously block the calling thread. Nested calls to + /// `run()` may be permitted, however it is not permitted to call `run()` on a + /// single executor instance from more than one thread. func run() throws - /// Signal to the runloop to stop running and return. + /// Run the executor's run loop until a condition is satisfied. + /// + /// Not every `RunLoopExecutor` will support this method; you must not call + /// it unless you *know* that it is supported. The default implementation + /// generates a fatal error. + /// + /// Parameters: + /// + /// - until condition: A closure that returns `true` if the run loop should + /// stop. + func run(until condition: () -> Bool) throws + + /// Signal to the run loop to stop running and return. /// /// This method may be called from the same thread that is in the `run()` - /// method, or from some other thread. It will not wait for the run loop - /// to stop; calling this method simply signals that the run loop *should*, - /// as soon as is practicable, stop the innermost `run()` invocation - /// and make that `run()` invocation return. + /// method, or from some other thread. It will not wait for the run loop to + /// stop; calling this method simply signals that the run loop *should*, as + /// soon as is practicable, stop the innermost `run()` invocation and make + /// that `run()` invocation return. func stop() } ``` @@ -279,44 +301,61 @@ protocol MainExecutor: RunLoopExecutor & SerialExecutor & EventableExecutor { } ``` +This cannot be a typealias because those will not work for Embedded Swift. + We will then expose properties on `MainActor` and `Task` to allow -users to query or set the executors: +users to query the executors: ```swift extension MainActor { /// The main executor, which is started implicitly by the `async main` /// entry point and owns the "main" thread. - /// - /// Attempting to set this after the first `enqueue` on the main - /// executor is a fatal error. - public static var executor: any MainExecutor { get set } + public static var executor: any MainExecutor { get } } extension Task { /// The default or global executor, which is the default place in which /// we run tasks. - /// - /// Attempting to set this after the first `enqueue` on the global - /// executor is a fatal error. - public static var defaultExecutor: any TaskExecutor { get set } + public static var defaultExecutor: any TaskExecutor { get } } ``` -The platform-specific default implementations of these two executors will also be -exposed with the names below: +There will also be an `ExecutorFactory` protocol, which is used to set +the default executors: -``` swift -/// The default main executor implementation for the current platform. -public struct PlatformMainExecutor: MainExecutor { - ... +```swift +/// An ExecutorFactory is used to create the default main and task +/// executors. +public protocol ExecutorFactory { + /// Constructs and returns the main executor, which is started implicitly + /// by the `async main` entry point and owns the "main" thread. + static var mainExecutor: any MainExecutor { get } + + /// Constructs and returns the default or global executor, which is the + /// default place in which we run tasks. + static var defaultExecutor: any TaskExecutor { get } } -/// The default global executor implementation for the current platform. -public struct PlatformDefaultExecutor: TaskExecutor { - ... +``` + +along with a default implementation of `ExecutorFactory` called +`PlatformExecutorFactory` that sets the default executors for the +current platform. + +Additionally, `Task` will expose a new `currentExecutor` property: + +```swift +extension Task { + /// Get the current executor; this is the executor that the currently + /// executing task is executing on. + public static var currentExecutor: (any Executor)? { get } } ``` +to allow `Task.sleep()` to wait on the appropriate executor, rather +than its current behaviour of always waiting on the global executor, +which adds unnecessary executor hops and context switches. + We will also need to expose the executor storage fields on `ExecutorJob`, so that they are accessible to Swift implementations of the `Executor` protocols: @@ -325,12 +364,19 @@ the `Executor` protocols: struct ExecutorJob { ... - /// Storage reserved for the executor - public var executorPrivate: (UInt, UInt) + /// Execute a closure, passing it the bounds of the executor private data + /// for the job. + /// + /// Parameters: + /// + /// - body: The closure to execute. + /// + /// Returns the result of executing the closure. + public func withUnsafeExecutorPrivateData(body: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R /// Kinds of schedulable jobs. @frozen - public struct Kind: Sendable { + public struct Kind: Sendable, RawRepresentable { public typealias RawValue = UInt8 /// The raw job kind value. @@ -344,7 +390,7 @@ struct ExecutorJob { } /// What kind of job this is - public var kind: Kind + public var kind: Kind { get } ... } ``` @@ -429,38 +475,191 @@ if let chunk = job.allocator?.allocate(capacity: 1024) { } ``` -### Embedded Swift +We will also round-out the `Executor` protocol with some `Clock`-based +APIs to enqueue after a delay: + +```swift +protocol Executor { + ... + /// `true` if this Executor supports scheduling. + /// + /// This will default to false. If you attempt to use the delayed + /// enqueuing functions on an executor that does not support scheduling, + /// the default executor will be used to do the scheduling instead, + /// unless the default executor does not support scheduling in which + /// case you will get a fatal error. + var supportsScheduling: Bool { get } + + /// Enqueue a job to run after a specified delay. + /// + /// You need only implement one of the two enqueue functions here; + /// the default implementation for the other will then call the one + /// you have implemented. + /// + /// Parameters: + /// + /// - job: The job to schedule. + /// - after: A `Duration` specifying the time after which the job + /// is to run. The job will not be executed before this + /// time has elapsed. + /// - tolerance: The maximum additional delay permissible before the + /// job is executed. `nil` means no limit. + /// - clock: The clock used for the delay. + func enqueue(_ job: consuming ExecutorJob, + after delay: C.Duration, + tolerance: C.Duration?, + clock: C) + + /// Enqueue a job to run at a specified time. + /// + /// You need only implement one of the two enqueue functions here; + /// the default implementation for the other will then call the one + /// you have implemented. + /// + /// Parameters: + /// + /// - job: The job to schedule. + /// - at: The `Instant` at which the job should run. The job + /// will not be executed before this time. + /// - tolerance: The maximum additional delay permissible before the + /// job is executed. `nil` means no limit. + /// - clock: The clock used for the delay.. + func enqueue(_ job: consuming ExecutorJob, + at instant: C.Instant, + tolerance: C.Duration?, + clock: C) + ... +} +``` + +As an implementer, you will only need to implement _one_ of the two +APIs to get both of them working; there is a default implementation +that will do the necessary mathematics for you to implement the other +one. + +If you try to call the `Clock`-based `enqueue` APIs on an executor +that does not declare support for them (by returning `true` from its +`supportsScheduling` property), the runtime will raise a fatal error. -For Embedded Swift we will provide default implementations of the main -and default executor that call C functions; this means that Embedded -Swift users can choose to implement those C functions to override the -default behaviour. This is desirable because Swift is not designed to -support externally defined Swift functions, types or methods in the -same way that C is. +(These functions have been added to the `Executor` protocol directly +rather than adding a separate protocol to avoid having to do a dynamic +cast at runtime, which is a relatively slow operation.) -We will also add a compile-time option to the Concurrency runtime to -allow users of Embedded Swift to disable the ability to dynamically -set the executors, as this is an option that may not be necessary in -that case. When this option is enabled, the `executor` and -`defaultExecutor` properties will be as follows (rather than using -existentials): +To support these `Clock`-based APIs, we will add to the `Clock` +protocol as follows: ```swift -extension MainActor { - /// The main executor, which is started implicitly by the `async main` - /// entry point and owns the "main" thread. - public static var executor: PlatformMainExecutor { get } +protocol Clock { + ... + /// The traits associated with this clock instance. + var traits: ClockTraits { get } + + /// Convert a Clock-specific Duration to a Swift Duration + /// + /// Some clocks may define `C.Duration` to be something other than a + /// `Swift.Duration`, but that makes it tricky to convert timestamps + /// between clocks, which is something we want to be able to support. + /// This method will convert whatever `C.Duration` is to a `Swift.Duration`. + /// + /// Parameters: + /// + /// - from duration: The `Duration` to convert + /// + /// Returns: A `Swift.Duration` representing the equivalent duration, or + /// `nil` if this function is not supported. + func convert(from duration: Duration) -> Swift.Duration? + + /// Convert a Swift Duration to a Clock-specific Duration + /// + /// Parameters: + /// + /// - from duration: The `Swift.Duration` to convert. + /// + /// Returns: A `Duration` representing the equivalent duration, or + /// `nil` if this function is not supported. + func convert(from duration: Swift.Duration) -> Duration? + + /// Convert an `Instant` from some other clock's `Instant` + /// + /// Parameters: + /// + /// - instant: The instant to convert. + // - from clock: The clock to convert from. + /// + /// Returns: An `Instant` representing the equivalent instant, or + /// `nil` if this function is not supported. + func convert(instant: OtherClock.Instant, + from clock: OtherClock) -> Instant? + ... } +``` -extension Task { - /// The default or global executor, which is the default place in which - /// we run tasks. - public static var defaultExecutor: PlatformDefaultExecutor { get } +If your `Clock` uses `Swift.Duration` as its `Duration` type, the +`convert(from duration:)` methods will be implemented for you. There +is also a default implementation of the `Instant` conversion method +that makes use of the `Duration` conversion methods. + +The `traits` property is of type `ClockTraits`, which is an +`OptionSet` as follows: + +```swift +/// Represents traits of a particular Clock implementation. +/// +/// Clocks may be of a number of different varieties; executors will likely +/// have specific clocks that they can use to schedule jobs, and will +/// therefore need to be able to convert timestamps to an appropriate clock +/// when asked to enqueue a job with a delay or deadline. +/// +/// Choosing a clock in general requires the ability to tell which of their +/// clocks best matches the clock that the user is trying to specify a +/// time or delay in. Executors are expected to do this on a best effort +/// basis. +@available(SwiftStdlib 6.2, *) +public struct ClockTraits: OptionSet { + public let rawValue: Int32 + + public init(rawValue: Int32) + + /// Clocks with this trait continue running while the machine is asleep. + public static let continuous = ... + + /// Indicates that a clock's time will only ever increase. + public static let monotonic = ... + + /// Clocks with this trait are tied to "wall time". + public static let wallTime = ... +} +``` + +Clock traits can be used by executor implementations to select the +most appropriate clock that they know how to wait on; they can then +use the `convert()` method above to convert the `Instant` or +`Duration` to that clock in order to actually enqueue a job. + +`ContinuousClock` and `SuspendingClock` will be updated to support +these new features. + +We will also add a way to test if an executor is the main executor: + +```swift +protocol Executor { + ... + /// `true` if this is the main executor. + var isMainExecutor: Bool { get } + ... } ``` -If this option is enabled, an Embedded Swift program that wishes to -customize executor behaviour will have to use the C API. +### Embedded Swift + +As we are not proposing to remove the existing "hook function" API +from Concurrency at this point, it will still be possible to implement +an executor for Embedded Swift by implementing the `Impl` functions in +C/C++. + +We will not be able to support the new `Clock`-based `enqueue` APIs on +Embedded Swift at present because it does not allow protocols to +contain generic functions. ### Coalesced Event Interface @@ -487,7 +686,7 @@ protocol EventableExecutor { /// - handler: The handler to call when the event fires. /// /// Returns a new opaque `Event`. - public func registerEvent(handler: @escaping () -> ()) -> Event + public func registerEvent(handler: @escaping @Sendable () -> ()) -> Event /// Deregister the given event. /// @@ -515,6 +714,33 @@ to coalesce these events, such that the handler will be triggered once for a potentially long series of `MainActor.executor.notify(event:)` invocations. +### Overriding the main and default executors + +Setting the executors directly is tricky because they might already be +in use somehow, and it is difficult in general to detect when that +might have happened. Instead, to specify different executors you will +implement your own `ExecutorFactory`, e.g. + +```swift +struct MyExecutorFactory: ExecutorFactory { + static var mainExecutor: any MainExecutor { return MyMainExecutor() } + static var defaultExecutor: any TaskExecutor { return MyTaskExecutor() } +} +``` + +then build your program with the `--executor-factory +MyModule.MyExecutorFactory` option. If you do not specify the module +for your executor factory, the compiler will look for it in the main +module. + +One might imagine a future where NIO provides executors of its own +where you can build with `--executor-factory SwiftNIO.ExecutorFactory` +to take advantage of those executors. + +We will also add an `executorFactory` option in SwiftPM's +`swiftSettings` to let people specify the executor factory in their +package manifests. + ## Detailed design ### `async` main code generation @@ -524,23 +750,37 @@ to something like ```swift func _main1() { + _swift_createExecutors(MyModule.MyExecutorFactory.self) + let task = Task(_main2) + task.runSynchronously() + MainActor.unownedExecutor.enqueue(_main3) + _swift_task_asyncMainDrainQueue() +} + +func _main2() { ... print("Before the first await") - MainActor.executor.enqueue(_main2) - MainActor.executor.run() } -func _main2() { +func _main3() { foo() print("After the first await") ... } ``` +where the `_swift_createExecutors` function is responsible for calling +the methods on your executor factory. + +This new function will only be called where the target's minimum +system version is high enough to support custom executors. + ## Source compatibility There should be no source compatibility concerns, as this proposal is -purely additive from a source code perspective. +purely additive from a source code perspective---all new protocol +methods will have default implementations, so existing code should +just build and work. ## ABI compatibility @@ -648,17 +888,12 @@ break for places where Swift Concurrency already runs, because some existing code already knows that it is not really asynchronous until the first `await` in the main entry point. -### Building support for clocks into `Executor` - -While the existing C interfaces within Concurrency do associate clocks -with executors, there is in fact no real need to do this, and it's -only that way internally because Dispatch happens to handle timers and -it was easy to write the implementation this way. +### Putting the new Clock-based enqueue functions into a protocol -In reality, timer-based scheduling can be handled through some -appropriate platform-specific mechanism, and when the relevant timer -fires the task that was scheduled for a specific time can be enqueued -on an appropriate executor using the `enqueue()` method. +It would be cleaner to have the new Clock-based enqueue functions in a +separate `SchedulingExecutor` protocol. However, if we did that, we +would need to add `as? SchedulingExecutor` runtime casts in various +places in the code, and dynamic casts can be expensive. ## Acknowledgments From 75b1b89aab9d5c06e17e8e4ad69f8d6969b413c2 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Thu, 13 Mar 2025 13:59:57 +0000 Subject: [PATCH 05/10] Remove EventableExecutor, further updates from pitch. Remove `EventableExecutor`; we can come back to that later. Update documentation for `currentExecutor` and add `preferredExecutor` and `currentSchedulableExecutor`. Move the clock-based enqueuing to a separate `SchedulableExecutor` protocol, and provide an efficient way to get it. This means we don't need the `supportsScheduling` property. Back `ClockTraits` with `UInt32`. --- .../nnnn-custom-main-and-global-executors.md | 125 +++++++----------- 1 file changed, 48 insertions(+), 77 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 90e8c6a4f1..dd71f7b877 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -293,11 +293,10 @@ public protocol RunLoopExecutor: Executor { } ``` -We will also add a protocol for the main actor's executor (see later -for details of `EventableExecutor` and why it exists): +We will also add a protocol for the main actor's executor: ```swift -protocol MainExecutor: RunLoopExecutor & SerialExecutor & EventableExecutor { +protocol MainExecutor: RunLoopExecutor & SerialExecutor { } ``` @@ -342,13 +341,33 @@ along with a default implementation of `ExecutorFactory` called `PlatformExecutorFactory` that sets the default executors for the current platform. -Additionally, `Task` will expose a new `currentExecutor` property: +Additionally, `Task` will expose a new `currentExecutor` property, as +well as properties for the `preferredExecutor` and the +`currentSchedulableExecutor`: ```swift extension Task { /// Get the current executor; this is the executor that the currently /// executing task is executing on. - public static var currentExecutor: (any Executor)? { get } + /// + /// This will return, in order of preference: + /// + /// 1. The custom executor associated with an `Actor` on which we are + /// currently running, or + /// 2. The preferred executor for the currently executing `Task`, or + /// 3. The task executor for the current thread + /// 4. The default executor. + public static var currentExecutor: any Executor { get } + + /// Get the preferred executor for the current `Task`, if any. + public static var preferredExecutor: (any TaskExecutor)? { get } + + /// Get the current *schedulable* executor, if any. + /// + /// This follows the same logic as `currentExecutor`, except that it ignores + /// any executor that isn't a `SchedulableExecutor`, and as such it may + /// eventually return `nil`. + public static var currentSchedulableExecutor: (any SchedulableExecutor)? { get } } ``` @@ -475,21 +494,24 @@ if let chunk = job.allocator?.allocate(capacity: 1024) { } ``` -We will also round-out the `Executor` protocol with some `Clock`-based -APIs to enqueue after a delay: +We will also add a `SchedulableExecutor` protocol as well as a way to +get it efficiently from an `Executor`: ```swift protocol Executor { ... - /// `true` if this Executor supports scheduling. + /// Return this executable as a SchedulableExecutor, or nil if that is + /// unsupported. /// - /// This will default to false. If you attempt to use the delayed - /// enqueuing functions on an executor that does not support scheduling, - /// the default executor will be used to do the scheduling instead, - /// unless the default executor does not support scheduling in which - /// case you will get a fatal error. - var supportsScheduling: Bool { get } + /// Executors can implement this method explicitly to avoid the use of + /// a potentially expensive runtime cast. + @available(SwiftStdlib 6.2, *) + var asSchedulable: AsSchedulable? { get } + ... +} +protocol SchedulableExecutor: Executor { + ... /// Enqueue a job to run after a specified delay. /// /// You need only implement one of the two enqueue functions here; @@ -537,14 +559,6 @@ APIs to get both of them working; there is a default implementation that will do the necessary mathematics for you to implement the other one. -If you try to call the `Clock`-based `enqueue` APIs on an executor -that does not declare support for them (by returning `true` from its -`supportsScheduling` property), the runtime will raise a fatal error. - -(These functions have been added to the `Executor` protocol directly -rather than adding a separate protocol to avoid having to do a dynamic -cast at runtime, which is a relatively slow operation.) - To support these `Clock`-based APIs, we will add to the `Clock` protocol as follows: @@ -616,9 +630,9 @@ The `traits` property is of type `ClockTraits`, which is an /// basis. @available(SwiftStdlib 6.2, *) public struct ClockTraits: OptionSet { - public let rawValue: Int32 + public let rawValue: UInt32 - public init(rawValue: Int32) + public init(rawValue: UInt32) /// Clocks with this trait continue running while the machine is asleep. public static let continuous = ... @@ -661,59 +675,6 @@ We will not be able to support the new `Clock`-based `enqueue` APIs on Embedded Swift at present because it does not allow protocols to contain generic functions. -### Coalesced Event Interface - -We would like custom main executors to be able to integrate with other -libraries, without tying the implementation to a specific library; in -practice, this means that the executor will need to be able to trigger -processing from some external event. - -```swift -protocol EventableExecutor { - - /// An opaque, executor-dependent type used to represent an event. - associatedtype Event - - /// Register a new event with a given handler. - /// - /// Notifying the executor of the event will cause the executor to - /// execute the handler, however the executor is free to coalesce multiple - /// event notifications, and is also free to execute the handler at a time - /// of its choosing. - /// - /// Parameters - /// - /// - handler: The handler to call when the event fires. - /// - /// Returns a new opaque `Event`. - public func registerEvent(handler: @escaping @Sendable () -> ()) -> Event - - /// Deregister the given event. - /// - /// After this function returns, there will be no further executions of the - /// handler for the given event. - public func deregister(event: Event) - - /// Notify the executor of an event. - /// - /// This will trigger, at some future point, the execution of the associated - /// event handler. Prior to that time, multiple calls to `notify` may be - /// coalesced and result in a single invocation of the event handler. - public func notify(event: Event) - -} -``` - -Our expectation is that a library that wishes to integrate with the -main executor will register an event with the main executor, and can -then notify the main executor of that event, which will trigger the -executor to run the associated handler at an appropriate time. - -The point of this interface is that a library can rely on the executor -to coalesce these events, such that the handler will be triggered once -for a potentially long series of `MainActor.executor.notify(event:)` -invocations. - ### Overriding the main and default executors Setting the executors directly is tricky because they might already be @@ -888,6 +849,16 @@ break for places where Swift Concurrency already runs, because some existing code already knows that it is not really asynchronous until the first `await` in the main entry point. +### Adding a coalesced event interface + +A previous revision of this proposal included an `EventableExecutor` +interface, which could be used to tie other libraries into a custom +executor without the custom executor needing to have specific +knowledge of those libraries. + +While a good idea, it was decided that this would be better dealt with +as a separate proposal. + ### Putting the new Clock-based enqueue functions into a protocol It would be cleaner to have the new Clock-based enqueue functions in a From 4bdc1e477d2a3a1c88f2a12e2235f228d26e1566 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Tue, 18 Mar 2025 18:14:22 +0000 Subject: [PATCH 06/10] Use typed throws rather than `rethrows`. We should use typed throws, which will make this work better for Embedded Swift. --- proposals/nnnn-custom-main-and-global-executors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index dd71f7b877..8f0b4b6f8a 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -391,7 +391,7 @@ struct ExecutorJob { /// - body: The closure to execute. /// /// Returns the result of executing the closure. - public func withUnsafeExecutorPrivateData(body: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R + public func withUnsafeExecutorPrivateData(body: (UnsafeMutableRawBufferPointer) throws(E) -> R) throws(E) -> R /// Kinds of schedulable jobs. @frozen From e9972e9d51b2f38e2cdc44271ade1b56f8ec7cc4 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Tue, 24 Jun 2025 14:59:42 +0100 Subject: [PATCH 07/10] Updates after second SE pitch. Removed clock conversion functions; we're going to add `enqueue` and `run` functions instead. Updated comments for various items. Mention that we're planning to expose the built-in executor implementations by name. --- .../nnnn-custom-main-and-global-executors.md | 226 ++++++++++-------- 1 file changed, 127 insertions(+), 99 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 8f0b4b6f8a..073ea5c501 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -296,7 +296,7 @@ public protocol RunLoopExecutor: Executor { We will also add a protocol for the main actor's executor: ```swift -protocol MainExecutor: RunLoopExecutor & SerialExecutor { +protocol MainExecutor: RunLoopExecutor, SerialExecutor { } ``` @@ -343,7 +343,7 @@ current platform. Additionally, `Task` will expose a new `currentExecutor` property, as well as properties for the `preferredExecutor` and the -`currentSchedulableExecutor`: +`currentSchedulingExecutor`: ```swift extension Task { @@ -362,12 +362,12 @@ extension Task { /// Get the preferred executor for the current `Task`, if any. public static var preferredExecutor: (any TaskExecutor)? { get } - /// Get the current *schedulable* executor, if any. + /// Get the current *scheduling* executor, if any. /// /// This follows the same logic as `currentExecutor`, except that it ignores - /// any executor that isn't a `SchedulableExecutor`, and as such it may + /// any executor that isn't a `SchedulingExecutor`, and as such it may /// eventually return `nil`. - public static var currentSchedulableExecutor: (any SchedulableExecutor)? { get } + public static var currentSchedulingExecutor: (any SchedulingExecutor)? { get } } ``` @@ -384,7 +384,9 @@ struct ExecutorJob { ... /// Execute a closure, passing it the bounds of the executor private data - /// for the job. + /// for the job. The executor is responsible for ensuring that any resources + /// referenced from the private data area are cleared up prior to running the + /// job. /// /// Parameters: /// @@ -393,7 +395,7 @@ struct ExecutorJob { /// Returns the result of executing the closure. public func withUnsafeExecutorPrivateData(body: (UnsafeMutableRawBufferPointer) throws(E) -> R) throws(E) -> R - /// Kinds of schedulable jobs. + /// Kinds of scheduling jobs. @frozen public struct Kind: Sendable, RawRepresentable { public typealias RawValue = UInt8 @@ -416,7 +418,7 @@ struct ExecutorJob { Finally, jobs of type `ExecutorJob.Kind.task` have the ability to allocate task memory, using a stack disciplined allocator; this memory -is automatically released when the task itself is released. +must be released _in reverse order_ before the job is executed. Rather than require users to test the job kind to discover this, which would mean that they would not be able to use allocation on new job @@ -437,13 +439,11 @@ extension ExecutorJob { /// A job-local stack-disciplined allocator. /// /// This can be used to allocate additional data required by an - /// executor implementation; memory allocated in this manner will - /// be released automatically when the job is disposed of by the - /// runtime. + /// executor implementation; memory allocated in this manner must + /// be released by the executor before the job is executed. /// /// N.B. Because this allocator is stack disciplined, explicitly - /// deallocating memory will also deallocate all memory allocated - /// after the block being deallocated. + /// deallocating memory out-of-order will cause your program to abort. struct LocalAllocator { /// Allocate a specified number of bytes of uninitialized memory. @@ -457,19 +457,16 @@ extension ExecutorJob { public func allocate(capacity: Int, as: T.Type) -> UnsafeMutableBufferPointer? - /// Deallocate previously allocated memory. Note that the task - /// allocator is stack disciplined, so if you deallocate a block of - /// memory, all memory allocated after that block is also deallocated. + /// Deallocate previously allocated memory. You must do this in + /// reverse order of allocations, prior to running the job. public func deallocate(_ buffer: UnsafeMutableRawBufferPointer?) - /// Deallocate previously allocated memory. Note that the task - /// allocator is stack disciplined, so if you deallocate a block of - /// memory, all memory allocated after that block is also deallocated. + /// Deallocate previously allocated memory. You must do this in + /// reverse order of allocations, prior to running the job. public func deallocate(_ pointer: UnsafeMutablePointer?) - /// Deallocate previously allocated memory. Note that the task - /// allocator is stack disciplined, so if you deallocate a block of - /// memory, all memory allocated after that block is also deallocated. + /// Deallocate previously allocated memory. You must do this in + /// reverse order of allocations, prior to running the job. public func deallocate(_ buffer: UnsafeMutableBufferPointer?) } @@ -494,23 +491,28 @@ if let chunk = job.allocator?.allocate(capacity: 1024) { } ``` -We will also add a `SchedulableExecutor` protocol as well as a way to +This feature is useful for executors that need to store additional +data alongside jobs that they currently have queued up. It is worth +re-emphasising that the data needs to be released, in reverse order +of allocation, prior to execution of the job to which it is attached. + +We will also add a `SchedulingExecutor` protocol as well as a way to get it efficiently from an `Executor`: ```swift protocol Executor { ... - /// Return this executable as a SchedulableExecutor, or nil if that is + /// Return this executable as a SchedulingExecutor, or nil if that is /// unsupported. /// /// Executors can implement this method explicitly to avoid the use of /// a potentially expensive runtime cast. @available(SwiftStdlib 6.2, *) - var asSchedulable: AsSchedulable? { get } + var asSchedulingExecutor: (any SchedulingExecutor)? { get } ... } -protocol SchedulableExecutor: Executor { +protocol SchedulingExecutor: Executor { ... /// Enqueue a job to run after a specified delay. /// @@ -565,101 +567,116 @@ protocol as follows: ```swift protocol Clock { ... - /// The traits associated with this clock instance. - var traits: ClockTraits { get } - - /// Convert a Clock-specific Duration to a Swift Duration - /// - /// Some clocks may define `C.Duration` to be something other than a - /// `Swift.Duration`, but that makes it tricky to convert timestamps - /// between clocks, which is something we want to be able to support. - /// This method will convert whatever `C.Duration` is to a `Swift.Duration`. + /// Run the given job on an unspecified executor at some point + /// after the given instant. /// /// Parameters: /// - /// - from duration: The `Duration` to convert + /// - job: The job we wish to run + /// - at instant: The time at which we would like it to run. + /// - tolerance: The ideal maximum delay we are willing to tolerate. /// - /// Returns: A `Swift.Duration` representing the equivalent duration, or - /// `nil` if this function is not supported. - func convert(from duration: Duration) -> Swift.Duration? + func run(_ job: consuming ExecutorJob, + at instant: Instant, tolerance: Duration?) - /// Convert a Swift Duration to a Clock-specific Duration + /// Enqueue the given job on the specified executor at some point after the + /// given instant. /// - /// Parameters: - /// - /// - from duration: The `Swift.Duration` to convert. - /// - /// Returns: A `Duration` representing the equivalent duration, or - /// `nil` if this function is not supported. - func convert(from duration: Swift.Duration) -> Duration? - - /// Convert an `Instant` from some other clock's `Instant` + /// The default implementation uses the `run` method to trigger a job that + /// does `executor.enqueue(job)`. If a particular `Clock` knows that the + /// executor it has been asked to use is the same one that it will run jobs + /// on, it can short-circuit this behaviour and directly use `run` with + /// the original job. /// /// Parameters: /// - /// - instant: The instant to convert. - // - from clock: The clock to convert from. + /// - job: The job we wish to run + /// - on executor: The executor on which we would like it to run. + /// - at instant: The time at which we would like it to run. + /// - tolerance: The ideal maximum delay we are willing to tolerate. /// - /// Returns: An `Instant` representing the equivalent instant, or - /// `nil` if this function is not supported. - func convert(instant: OtherClock.Instant, - from clock: OtherClock) -> Instant? + func enqueue(_ job: consuming ExecutorJob, + on executor: some Executor, + at instant: Instant, tolerance: Duration?) ... } ``` -If your `Clock` uses `Swift.Duration` as its `Duration` type, the -`convert(from duration:)` methods will be implemented for you. There -is also a default implementation of the `Instant` conversion method -that makes use of the `Duration` conversion methods. +There is a default implementation of the `enqueue` method on `Clock`, +which calls the `run` method; if you attempt to use a `Clock` with an +executor that does not understand it, and that `Clock` does not +implement the `run` method, you will get a fatal error at runtime. -The `traits` property is of type `ClockTraits`, which is an -`OptionSet` as follows: +Executors that do not specifically recognise a particular clock may +choose instead to have their `enqueue(..., clock:)` methods call the +clock's `enqueue()` method; this will allow the clock to make an +appropriate decision as to how to proceed. -```swift -/// Represents traits of a particular Clock implementation. -/// -/// Clocks may be of a number of different varieties; executors will likely -/// have specific clocks that they can use to schedule jobs, and will -/// therefore need to be able to convert timestamps to an appropriate clock -/// when asked to enqueue a job with a delay or deadline. -/// -/// Choosing a clock in general requires the ability to tell which of their -/// clocks best matches the clock that the user is trying to specify a -/// time or delay in. Executors are expected to do this on a best effort -/// basis. -@available(SwiftStdlib 6.2, *) -public struct ClockTraits: OptionSet { - public let rawValue: UInt32 +We will also add a way to test if an executor is the main executor: - public init(rawValue: UInt32) +```swift +protocol Executor { + ... + /// `true` if this is the main executor. + var isMainExecutor: Bool { get } + ... +} +``` - /// Clocks with this trait continue running while the machine is asleep. - public static let continuous = ... +Finally, we will expose the following built-in executor +implementations: - /// Indicates that a clock's time will only ever increase. - public static let monotonic = ... +```swift +/// A Dispatch-based main executor (not on Embedded or WASI) +@available(StdlibDeploymentTarget 6.2, *) +public class DispatchMainExecutor: MainExecutor, + SchedulingExecutor, + @unchecked Sendable { + ... +} - /// Clocks with this trait are tied to "wall time". - public static let wallTime = ... +/// A Dispatch-based `TaskExecutor` (not on Embedded or WASI) +@available(StdlibDeploymentTarget 6.2, *) +public class DispatchGlobalTaskExecutor: TaskExecutor, + SchedulingExecutor, + @unchecked Sendable { + ... } -``` -Clock traits can be used by executor implementations to select the -most appropriate clock that they know how to wait on; they can then -use the `convert()` method above to convert the `Instant` or -`Duration` to that clock in order to actually enqueue a job. +/// A CFRunLoop-based main executor (Apple platforms only) +@available(StdlibDeploymentTarget 6.2, *) +public final class CFMainExecutor: DispatchMainExecutor, + @unchecked Sendable { + ... +} -`ContinuousClock` and `SuspendingClock` will be updated to support -these new features. +/// A `TaskExecutor` to match `CFMainExecutor` (Apple platforms only) +@available(StdlibDeploymentTarget 6.2, *) +public final class CFTaskExecutor: DispatchGlobalTaskExecutor, + @unchecked Sendable { + ... +} -We will also add a way to test if an executor is the main executor: +/// A co-operative executor that can be used as the main executor or as a +/// task executor. Tasks scheduled on this executor will run on the thread +/// that called `run()`. +/// +/// Note that this executor will not be thread-safe on Embedded Swift. +@available(StdlibDeploymentTarget 6.2, *) +class CooperativeExecutor: MainExecutor, + TaskExecutor, + SchedulingExecutor, + @unchecked Sendable { + ... +} -```swift -protocol Executor { +/// A main executor that calls fatalError(). +class UnimplementedMainExecutor: MainExecutor, @unchecked Sendable { ... - /// `true` if this is the main executor. - var isMainExecutor: Bool { get } +} + +/// A task executor that calls fatalError(). +class UnimplementedTaskExecutor: TaskExecutor, @unchecked Sendable { ... } ``` @@ -702,9 +719,7 @@ We will also add an `executorFactory` option in SwiftPM's `swiftSettings` to let people specify the executor factory in their package manifests. -## Detailed design - -### `async` main code generation +## `async` main code generation The compiler's code generation for `async` main functions will change to something like @@ -859,13 +874,26 @@ knowledge of those libraries. While a good idea, it was decided that this would be better dealt with as a separate proposal. -### Putting the new Clock-based enqueue functions into a protocol +### Putting the new `Clock`-based enqueue functions into a protocol It would be cleaner to have the new Clock-based enqueue functions in a separate `SchedulingExecutor` protocol. However, if we did that, we would need to add `as? SchedulingExecutor` runtime casts in various places in the code, and dynamic casts can be expensive. +### Adding special support for canonicalizing `Clock`s + +There are situations where you might create a derived `Clock`, that is +implemented under the covers by reference to some other clock. One +way to support that might be to add a `canonicalClock` property that +you can fetch to obtain the underlying clock, then provide conversion +functions to convert `Instant` and `Duration` values as appropriate. + +After implementing this, it became apparent that it wasn't really +necessary and complicated the API without providing any significant +additional capability. A derived `Clock` can simply implement the +`run` and/or `enqueue` methods instead. + ## Acknowledgments Thanks to Cory Benfield, Franz Busch, David Greenaway, Rokhini Prabhu, From 90e5d1b18fd532cec2959ea144aa7f03dbe30c6a Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Tue, 24 Jun 2025 15:23:29 +0100 Subject: [PATCH 08/10] Add link to third pitch. --- proposals/nnnn-custom-main-and-global-executors.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 073ea5c501..fe2ef9193d 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -6,7 +6,11 @@ * Review Manager: TBD * Status: **Pitch, Awaiting Implementation** * Implementation: TBA -* Review: ([original pitch](https://forums.swift.org/t/pitch-custom-main-and-global-executors/77247) ([pitch])(https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437) +* Review: ([first + pitch](https://forums.swift.org/t/pitch-custom-main-and-global-executors/77247)) + ([second + pitch](https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437)) + ([third pitch](https://forums.swift.org/t/pitch-3-custom-main-and-global-executors/80638)) ## Introduction From 415a6158e766c3810af572008f53c35d4013c7d0 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Wed, 2 Jul 2025 15:08:00 +0100 Subject: [PATCH 09/10] Further proposal updates on the basis of feedback received. Allow `DefaultExecutorFactory` to be defined in the `@main` struct as well as at top level. Remove some text from the proposal that is out of date. Don't expose the Dispatch or CF executors, since that implies that we will have a permanent dependency on Dispatch or Foundation. --- .../nnnn-custom-main-and-global-executors.md | 87 ++++++------------- 1 file changed, 26 insertions(+), 61 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index fe2ef9193d..358951ac00 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -631,56 +631,26 @@ Finally, we will expose the following built-in executor implementations: ```swift -/// A Dispatch-based main executor (not on Embedded or WASI) -@available(StdlibDeploymentTarget 6.2, *) -public class DispatchMainExecutor: MainExecutor, - SchedulingExecutor, - @unchecked Sendable { - ... -} - -/// A Dispatch-based `TaskExecutor` (not on Embedded or WASI) -@available(StdlibDeploymentTarget 6.2, *) -public class DispatchGlobalTaskExecutor: TaskExecutor, - SchedulingExecutor, - @unchecked Sendable { - ... -} - -/// A CFRunLoop-based main executor (Apple platforms only) -@available(StdlibDeploymentTarget 6.2, *) -public final class CFMainExecutor: DispatchMainExecutor, - @unchecked Sendable { - ... -} - -/// A `TaskExecutor` to match `CFMainExecutor` (Apple platforms only) -@available(StdlibDeploymentTarget 6.2, *) -public final class CFTaskExecutor: DispatchGlobalTaskExecutor, - @unchecked Sendable { - ... -} - /// A co-operative executor that can be used as the main executor or as a /// task executor. Tasks scheduled on this executor will run on the thread /// that called `run()`. /// /// Note that this executor will not be thread-safe on Embedded Swift. @available(StdlibDeploymentTarget 6.2, *) -class CooperativeExecutor: MainExecutor, - TaskExecutor, - SchedulingExecutor, - @unchecked Sendable { +final class CooperativeExecutor: MainExecutor, + TaskExecutor, + SchedulingExecutor, + @unchecked Sendable { ... } /// A main executor that calls fatalError(). -class UnimplementedMainExecutor: MainExecutor, @unchecked Sendable { +final class UnimplementedMainExecutor: MainExecutor, @unchecked Sendable { ... } /// A task executor that calls fatalError(). -class UnimplementedTaskExecutor: TaskExecutor, @unchecked Sendable { +final class UnimplementedTaskExecutor: TaskExecutor, @unchecked Sendable { ... } ``` @@ -710,18 +680,24 @@ struct MyExecutorFactory: ExecutorFactory { } ``` -then build your program with the `--executor-factory -MyModule.MyExecutorFactory` option. If you do not specify the module -for your executor factory, the compiler will look for it in the main -module. +then declare a `typealias` as follows: + +```swift +typealias DefaultExecutorFactory = MyExecutorFactory +``` + +The compiler will look in the following locations for the default +executor factory, in the order specified below: + +1. The `@main` type, if any. (This includes types defined by + protocols implemented by the `@main` `struct`.) + +2. The top level of the main module. -One might imagine a future where NIO provides executors of its own -where you can build with `--executor-factory SwiftNIO.ExecutorFactory` -to take advantage of those executors. +3. The Concurrency runtime itself. -We will also add an `executorFactory` option in SwiftPM's -`swiftSettings` to let people specify the executor factory in their -package manifests. +The first `DefaultExecutorFactory` type that it finds will be the one +that gets used. ## `async` main code generation @@ -789,15 +765,11 @@ think we want `run()` and `stop()` on `RunLoopExecutor`. ## Alternatives considered -### `typealias` in entry point struct +### Using a command line argument to select the default executors. -The idea here would be to have the `@main` `struct` declare the -executor type that it wants. - -This is straightforward for users, _but_ doesn't work for top-level -code, and also doesn't allow the user to change executor based on -configuration (e.g. "use the `epoll()` based executor, not the -`io_uring` based executor"), as it's fixed at compile time. +This was rejected in favour of a "magic `typealias`"; the latter is +better because it means that the program itself specifies which +executor it should be using. ### Adding a `createExecutor()` method to the entry point struct @@ -878,13 +850,6 @@ knowledge of those libraries. While a good idea, it was decided that this would be better dealt with as a separate proposal. -### Putting the new `Clock`-based enqueue functions into a protocol - -It would be cleaner to have the new Clock-based enqueue functions in a -separate `SchedulingExecutor` protocol. However, if we did that, we -would need to add `as? SchedulingExecutor` runtime casts in various -places in the code, and dynamic casts can be expensive. - ### Adding special support for canonicalizing `Clock`s There are situations where you might create a derived `Clock`, that is From 668a24bd868d7364943ba4a1a2c1b132af1d6437 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Thu, 3 Jul 2025 16:38:47 +0100 Subject: [PATCH 10/10] Remove CooperativeExecutor from the public API. We can have something similar in a separate package; let's not share this implementation outside of the runtime for now (it has a number of gotchas and we'd have to spend quite a bit of time in the proposal explaining what it's for and what it does). --- proposals/nnnn-custom-main-and-global-executors.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/proposals/nnnn-custom-main-and-global-executors.md b/proposals/nnnn-custom-main-and-global-executors.md index 358951ac00..e5bc92a9b8 100644 --- a/proposals/nnnn-custom-main-and-global-executors.md +++ b/proposals/nnnn-custom-main-and-global-executors.md @@ -631,19 +631,6 @@ Finally, we will expose the following built-in executor implementations: ```swift -/// A co-operative executor that can be used as the main executor or as a -/// task executor. Tasks scheduled on this executor will run on the thread -/// that called `run()`. -/// -/// Note that this executor will not be thread-safe on Embedded Swift. -@available(StdlibDeploymentTarget 6.2, *) -final class CooperativeExecutor: MainExecutor, - TaskExecutor, - SchedulingExecutor, - @unchecked Sendable { - ... -} - /// A main executor that calls fatalError(). final class UnimplementedMainExecutor: MainExecutor, @unchecked Sendable { ...