Skip to content

Commit e9972e9

Browse files
committed
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.
1 parent 4bdc1e4 commit e9972e9

File tree

1 file changed

+127
-99
lines changed

1 file changed

+127
-99
lines changed

proposals/nnnn-custom-main-and-global-executors.md

Lines changed: 127 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ public protocol RunLoopExecutor: Executor {
296296
We will also add a protocol for the main actor's executor:
297297

298298
```swift
299-
protocol MainExecutor: RunLoopExecutor & SerialExecutor {
299+
protocol MainExecutor: RunLoopExecutor, SerialExecutor {
300300
}
301301
```
302302

@@ -343,7 +343,7 @@ current platform.
343343

344344
Additionally, `Task` will expose a new `currentExecutor` property, as
345345
well as properties for the `preferredExecutor` and the
346-
`currentSchedulableExecutor`:
346+
`currentSchedulingExecutor`:
347347

348348
```swift
349349
extension Task {
@@ -362,12 +362,12 @@ extension Task {
362362
/// Get the preferred executor for the current `Task`, if any.
363363
public static var preferredExecutor: (any TaskExecutor)? { get }
364364

365-
/// Get the current *schedulable* executor, if any.
365+
/// Get the current *scheduling* executor, if any.
366366
///
367367
/// This follows the same logic as `currentExecutor`, except that it ignores
368-
/// any executor that isn't a `SchedulableExecutor`, and as such it may
368+
/// any executor that isn't a `SchedulingExecutor`, and as such it may
369369
/// eventually return `nil`.
370-
public static var currentSchedulableExecutor: (any SchedulableExecutor)? { get }
370+
public static var currentSchedulingExecutor: (any SchedulingExecutor)? { get }
371371
}
372372
```
373373

@@ -384,7 +384,9 @@ struct ExecutorJob {
384384
...
385385

386386
/// Execute a closure, passing it the bounds of the executor private data
387-
/// for the job.
387+
/// for the job. The executor is responsible for ensuring that any resources
388+
/// referenced from the private data area are cleared up prior to running the
389+
/// job.
388390
///
389391
/// Parameters:
390392
///
@@ -393,7 +395,7 @@ struct ExecutorJob {
393395
/// Returns the result of executing the closure.
394396
public func withUnsafeExecutorPrivateData<R, E>(body: (UnsafeMutableRawBufferPointer) throws(E) -> R) throws(E) -> R
395397

396-
/// Kinds of schedulable jobs.
398+
/// Kinds of scheduling jobs.
397399
@frozen
398400
public struct Kind: Sendable, RawRepresentable {
399401
public typealias RawValue = UInt8
@@ -416,7 +418,7 @@ struct ExecutorJob {
416418

417419
Finally, jobs of type `ExecutorJob.Kind.task` have the ability to
418420
allocate task memory, using a stack disciplined allocator; this memory
419-
is automatically released when the task itself is released.
421+
must be released _in reverse order_ before the job is executed.
420422

421423
Rather than require users to test the job kind to discover this, which
422424
would mean that they would not be able to use allocation on new job
@@ -437,13 +439,11 @@ extension ExecutorJob {
437439
/// A job-local stack-disciplined allocator.
438440
///
439441
/// This can be used to allocate additional data required by an
440-
/// executor implementation; memory allocated in this manner will
441-
/// be released automatically when the job is disposed of by the
442-
/// runtime.
442+
/// executor implementation; memory allocated in this manner must
443+
/// be released by the executor before the job is executed.
443444
///
444445
/// N.B. Because this allocator is stack disciplined, explicitly
445-
/// deallocating memory will also deallocate all memory allocated
446-
/// after the block being deallocated.
446+
/// deallocating memory out-of-order will cause your program to abort.
447447
struct LocalAllocator {
448448

449449
/// Allocate a specified number of bytes of uninitialized memory.
@@ -457,19 +457,16 @@ extension ExecutorJob {
457457
public func allocate<T>(capacity: Int, as: T.Type)
458458
-> UnsafeMutableBufferPointer<T>?
459459

460-
/// Deallocate previously allocated memory. Note that the task
461-
/// allocator is stack disciplined, so if you deallocate a block of
462-
/// memory, all memory allocated after that block is also deallocated.
460+
/// Deallocate previously allocated memory. You must do this in
461+
/// reverse order of allocations, prior to running the job.
463462
public func deallocate(_ buffer: UnsafeMutableRawBufferPointer?)
464463

465-
/// Deallocate previously allocated memory. Note that the task
466-
/// allocator is stack disciplined, so if you deallocate a block of
467-
/// memory, all memory allocated after that block is also deallocated.
464+
/// Deallocate previously allocated memory. You must do this in
465+
/// reverse order of allocations, prior to running the job.
468466
public func deallocate<T>(_ pointer: UnsafeMutablePointer<T>?)
469467

470-
/// Deallocate previously allocated memory. Note that the task
471-
/// allocator is stack disciplined, so if you deallocate a block of
472-
/// memory, all memory allocated after that block is also deallocated.
468+
/// Deallocate previously allocated memory. You must do this in
469+
/// reverse order of allocations, prior to running the job.
473470
public func deallocate<T>(_ buffer: UnsafeMutableBufferPointer<T>?)
474471

475472
}
@@ -494,23 +491,28 @@ if let chunk = job.allocator?.allocate(capacity: 1024) {
494491
}
495492
```
496493

497-
We will also add a `SchedulableExecutor` protocol as well as a way to
494+
This feature is useful for executors that need to store additional
495+
data alongside jobs that they currently have queued up. It is worth
496+
re-emphasising that the data needs to be released, in reverse order
497+
of allocation, prior to execution of the job to which it is attached.
498+
499+
We will also add a `SchedulingExecutor` protocol as well as a way to
498500
get it efficiently from an `Executor`:
499501

500502
```swift
501503
protocol Executor {
502504
...
503-
/// Return this executable as a SchedulableExecutor, or nil if that is
505+
/// Return this executable as a SchedulingExecutor, or nil if that is
504506
/// unsupported.
505507
///
506508
/// Executors can implement this method explicitly to avoid the use of
507509
/// a potentially expensive runtime cast.
508510
@available(SwiftStdlib 6.2, *)
509-
var asSchedulable: AsSchedulable? { get }
511+
var asSchedulingExecutor: (any SchedulingExecutor)? { get }
510512
...
511513
}
512514

513-
protocol SchedulableExecutor: Executor {
515+
protocol SchedulingExecutor: Executor {
514516
...
515517
/// Enqueue a job to run after a specified delay.
516518
///
@@ -565,101 +567,116 @@ protocol as follows:
565567
```swift
566568
protocol Clock {
567569
...
568-
/// The traits associated with this clock instance.
569-
var traits: ClockTraits { get }
570-
571-
/// Convert a Clock-specific Duration to a Swift Duration
572-
///
573-
/// Some clocks may define `C.Duration` to be something other than a
574-
/// `Swift.Duration`, but that makes it tricky to convert timestamps
575-
/// between clocks, which is something we want to be able to support.
576-
/// This method will convert whatever `C.Duration` is to a `Swift.Duration`.
570+
/// Run the given job on an unspecified executor at some point
571+
/// after the given instant.
577572
///
578573
/// Parameters:
579574
///
580-
/// - from duration: The `Duration` to convert
575+
/// - job: The job we wish to run
576+
/// - at instant: The time at which we would like it to run.
577+
/// - tolerance: The ideal maximum delay we are willing to tolerate.
581578
///
582-
/// Returns: A `Swift.Duration` representing the equivalent duration, or
583-
/// `nil` if this function is not supported.
584-
func convert(from duration: Duration) -> Swift.Duration?
579+
func run(_ job: consuming ExecutorJob,
580+
at instant: Instant, tolerance: Duration?)
585581

586-
/// Convert a Swift Duration to a Clock-specific Duration
582+
/// Enqueue the given job on the specified executor at some point after the
583+
/// given instant.
587584
///
588-
/// Parameters:
589-
///
590-
/// - from duration: The `Swift.Duration` to convert.
591-
///
592-
/// Returns: A `Duration` representing the equivalent duration, or
593-
/// `nil` if this function is not supported.
594-
func convert(from duration: Swift.Duration) -> Duration?
595-
596-
/// Convert an `Instant` from some other clock's `Instant`
585+
/// The default implementation uses the `run` method to trigger a job that
586+
/// does `executor.enqueue(job)`. If a particular `Clock` knows that the
587+
/// executor it has been asked to use is the same one that it will run jobs
588+
/// on, it can short-circuit this behaviour and directly use `run` with
589+
/// the original job.
597590
///
598591
/// Parameters:
599592
///
600-
/// - instant: The instant to convert.
601-
// - from clock: The clock to convert from.
593+
/// - job: The job we wish to run
594+
/// - on executor: The executor on which we would like it to run.
595+
/// - at instant: The time at which we would like it to run.
596+
/// - tolerance: The ideal maximum delay we are willing to tolerate.
602597
///
603-
/// Returns: An `Instant` representing the equivalent instant, or
604-
/// `nil` if this function is not supported.
605-
func convert<OtherClock: Clock>(instant: OtherClock.Instant,
606-
from clock: OtherClock) -> Instant?
598+
func enqueue(_ job: consuming ExecutorJob,
599+
on executor: some Executor,
600+
at instant: Instant, tolerance: Duration?)
607601
...
608602
}
609603
```
610604

611-
If your `Clock` uses `Swift.Duration` as its `Duration` type, the
612-
`convert(from duration:)` methods will be implemented for you. There
613-
is also a default implementation of the `Instant` conversion method
614-
that makes use of the `Duration` conversion methods.
605+
There is a default implementation of the `enqueue` method on `Clock`,
606+
which calls the `run` method; if you attempt to use a `Clock` with an
607+
executor that does not understand it, and that `Clock` does not
608+
implement the `run` method, you will get a fatal error at runtime.
615609

616-
The `traits` property is of type `ClockTraits`, which is an
617-
`OptionSet` as follows:
610+
Executors that do not specifically recognise a particular clock may
611+
choose instead to have their `enqueue(..., clock:)` methods call the
612+
clock's `enqueue()` method; this will allow the clock to make an
613+
appropriate decision as to how to proceed.
618614

619-
```swift
620-
/// Represents traits of a particular Clock implementation.
621-
///
622-
/// Clocks may be of a number of different varieties; executors will likely
623-
/// have specific clocks that they can use to schedule jobs, and will
624-
/// therefore need to be able to convert timestamps to an appropriate clock
625-
/// when asked to enqueue a job with a delay or deadline.
626-
///
627-
/// Choosing a clock in general requires the ability to tell which of their
628-
/// clocks best matches the clock that the user is trying to specify a
629-
/// time or delay in. Executors are expected to do this on a best effort
630-
/// basis.
631-
@available(SwiftStdlib 6.2, *)
632-
public struct ClockTraits: OptionSet {
633-
public let rawValue: UInt32
615+
We will also add a way to test if an executor is the main executor:
634616

635-
public init(rawValue: UInt32)
617+
```swift
618+
protocol Executor {
619+
...
620+
/// `true` if this is the main executor.
621+
var isMainExecutor: Bool { get }
622+
...
623+
}
624+
```
636625

637-
/// Clocks with this trait continue running while the machine is asleep.
638-
public static let continuous = ...
626+
Finally, we will expose the following built-in executor
627+
implementations:
639628

640-
/// Indicates that a clock's time will only ever increase.
641-
public static let monotonic = ...
629+
```swift
630+
/// A Dispatch-based main executor (not on Embedded or WASI)
631+
@available(StdlibDeploymentTarget 6.2, *)
632+
public class DispatchMainExecutor: MainExecutor,
633+
SchedulingExecutor,
634+
@unchecked Sendable {
635+
...
636+
}
642637

643-
/// Clocks with this trait are tied to "wall time".
644-
public static let wallTime = ...
638+
/// A Dispatch-based `TaskExecutor` (not on Embedded or WASI)
639+
@available(StdlibDeploymentTarget 6.2, *)
640+
public class DispatchGlobalTaskExecutor: TaskExecutor,
641+
SchedulingExecutor,
642+
@unchecked Sendable {
643+
...
645644
}
646-
```
647645

648-
Clock traits can be used by executor implementations to select the
649-
most appropriate clock that they know how to wait on; they can then
650-
use the `convert()` method above to convert the `Instant` or
651-
`Duration` to that clock in order to actually enqueue a job.
646+
/// A CFRunLoop-based main executor (Apple platforms only)
647+
@available(StdlibDeploymentTarget 6.2, *)
648+
public final class CFMainExecutor: DispatchMainExecutor,
649+
@unchecked Sendable {
650+
...
651+
}
652652

653-
`ContinuousClock` and `SuspendingClock` will be updated to support
654-
these new features.
653+
/// A `TaskExecutor` to match `CFMainExecutor` (Apple platforms only)
654+
@available(StdlibDeploymentTarget 6.2, *)
655+
public final class CFTaskExecutor: DispatchGlobalTaskExecutor,
656+
@unchecked Sendable {
657+
...
658+
}
655659

656-
We will also add a way to test if an executor is the main executor:
660+
/// A co-operative executor that can be used as the main executor or as a
661+
/// task executor. Tasks scheduled on this executor will run on the thread
662+
/// that called `run()`.
663+
///
664+
/// Note that this executor will not be thread-safe on Embedded Swift.
665+
@available(StdlibDeploymentTarget 6.2, *)
666+
class CooperativeExecutor: MainExecutor,
667+
TaskExecutor,
668+
SchedulingExecutor,
669+
@unchecked Sendable {
670+
...
671+
}
657672

658-
```swift
659-
protocol Executor {
673+
/// A main executor that calls fatalError().
674+
class UnimplementedMainExecutor: MainExecutor, @unchecked Sendable {
660675
...
661-
/// `true` if this is the main executor.
662-
var isMainExecutor: Bool { get }
676+
}
677+
678+
/// A task executor that calls fatalError().
679+
class UnimplementedTaskExecutor: TaskExecutor, @unchecked Sendable {
663680
...
664681
}
665682
```
@@ -702,9 +719,7 @@ We will also add an `executorFactory` option in SwiftPM's
702719
`swiftSettings` to let people specify the executor factory in their
703720
package manifests.
704721

705-
## Detailed design
706-
707-
### `async` main code generation
722+
## `async` main code generation
708723

709724
The compiler's code generation for `async` main functions will change
710725
to something like
@@ -859,13 +874,26 @@ knowledge of those libraries.
859874
While a good idea, it was decided that this would be better dealt with
860875
as a separate proposal.
861876

862-
### Putting the new Clock-based enqueue functions into a protocol
877+
### Putting the new `Clock`-based enqueue functions into a protocol
863878

864879
It would be cleaner to have the new Clock-based enqueue functions in a
865880
separate `SchedulingExecutor` protocol. However, if we did that, we
866881
would need to add `as? SchedulingExecutor` runtime casts in various
867882
places in the code, and dynamic casts can be expensive.
868883

884+
### Adding special support for canonicalizing `Clock`s
885+
886+
There are situations where you might create a derived `Clock`, that is
887+
implemented under the covers by reference to some other clock. One
888+
way to support that might be to add a `canonicalClock` property that
889+
you can fetch to obtain the underlying clock, then provide conversion
890+
functions to convert `Instant` and `Duration` values as appropriate.
891+
892+
After implementing this, it became apparent that it wasn't really
893+
necessary and complicated the API without providing any significant
894+
additional capability. A derived `Clock` can simply implement the
895+
`run` and/or `enqueue` methods instead.
896+
869897
## Acknowledgments
870898

871899
Thanks to Cory Benfield, Franz Busch, David Greenaway, Rokhini Prabhu,

0 commit comments

Comments
 (0)