From 5f24fafc31b1bdffd913095ec30d6d78e4c6094b Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Mon, 9 Dec 2024 12:07:12 +0000 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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