This is a second pitch for the ability to write custom default executors (both the main executor, as used for the main actor, and the global executor, used by default for Swift Tasks). You can find the previous pitch here.
The mechanism for selecting custom executors has changed; rather than being able to set MainActor.executor or Task.defaultExecutor (which would only have been possible very early in program start-up), you will specity an ExecutorFactory conforming type using a new compiler setting, -executor-factory. The ExecutorFactory will create and return MainExecutor and TaskExecutor instances as required.
Clock support has been added to the pitch. It will be possible after this work to implement a custom clock and use it with Task.sleep().
As before, editorial feedback can be raised against the PR itself, but any design feedback belongs in this thread.
Great update! Still very excited about this pitch.
extension MainActor {
/// The main executor, which is started implicitly by the `async main`
/// entry point and owns the "main" thread.
public static var executor: any MainExecutor { get }
}
Do we expect that we might want to add a var executor: SerialExecutor to actor similar to how we have the unownedExecutor? Just wondering if this might then become problematic regarding the naming and we should potentially rename the property on MainActor to avoid the conflict. My assumption was that we could add an executor property to Actor once we get the right lifetime annotations.
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 }
}
Can we expand the documentation here a bit what executor this could be e.g. can this be the preferred task executor or the global executor or the custom serial executor of the current isolation domain?
If this property is this dynamic in returning the current executor could we also add one to get the preferredExecutor please. This seems to fit into this proposal and we really need it to implement some of our async algorithms correctly.
public func withUnsafeExecutorPrivateData<R>(body: (UnsafeMutableRawBufferPointer) throws -> R) rethrows -> R
Can we use a more descriptive generic name here like Return? Also this method should probably adopt typed throws instead of using rethrows.
protocol EventableExecutor
I would love to understand this more. Can this be used to implement kqueue/epoll based event FD/timer FD wakeup mechanisms?
This cannot be a typealias because those will not work for Embedded Swift.
I don't see why Embedded Swift couldn't support this as a typealias, if you think that is the more correct design. Embedded Swift doesn't currently support arbitrary any Protocol existentials, but if it also rejects the use of protocol typealiases as generic constraints, that's a bug. Since Embedded Swift is still a work in progress, I don't think it's a good idea to shape implementation around its current implementation limitations as long as it's foreseeable that Embedded Swift could support something in principle.
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.)
I can think of a couple of ways to support these conditional requirements more soundly, while still avoiding the need to involve dynamic casting in the capability check path:
You could use an associated type to relate the Self type to itself with a refined protocol requirement, something like:
protocol Executor {
/// This Executor type as a schedulable executor.
///
/// If the conforming type also conforms to `Schedulable`, then this is
/// bound to `Self`. Otherwise, it is an uninhabited type (such as Never).
associatedtype AsSchedulable: SchedulableExecutor = Never
}
protocol SchedulableExecutor: Executor
where Self.AsSchedulable == Self { ... }
extension Executor {
/// Return this executable as a SchedulableExecutor, or nil if that is
/// unsupported.
var asSchedulable: AsSchedulable? {
if Self.self == AsSchedulable.self {
return _identityCast(self, as: AsSchedulable.self)
} else {
return nil
}
}
}
Looking up an associated type should be (after metadata instantiation) a single word read out of a witness table that's already readily accessible (or be fully specialized away when generic specialization is possible), as opposed to the global search that a dynamic cast does. This approach also preserves the ability for generic code to require T: SchedulableExecutor statically. Likewise, if you think it's important to also be able to efficiently test that an Executor is a SerialExecutor, TaskExecutor, RunLoopExecutor, EventableExecutor, etc., you could set up a similar associated type downlink to those related protocols as well. (This pattern is something I'd like us to have more streamlined language-level support for, since it comes up frequently that protocol towers want this sort of capability checking to be efficient.)
Alternatively, do the operations need to be generic on an arbitrary Clock, or would an executor typically have a favored Clock implementation it would want to use as an associated type? If these were formulated as:
then, in addition to this interface being more efficient for working with the preferred Clock type, an implementation could also communicate that it doesn't support scheduling by using Never as its Clock associated type, making these methods impossible to call. The clock-generic functions could perhaps still be provided as conveniences, as extension methods using the new conversion operations on the Clock protocol.
If viable, that approach might also help with other Embedded Swift interactions you noted further in the proposal:
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.
That isn't true; Embedded Swift doesn't allow classes to have non-final generic methods, and it can't invoke generic methods through (class-constrained) existentials, but generic requirements on protocols used as generic requirements are supported. To that second point, the reliance on any Executor existentials seems like the bigger barrier to fully realizing this proposal on Embedded Swift, since it seems undesirable for Executor to be class-constrained (though I hold out hope that Embedded Swift will support more general existentials in time as well).
Interesting point, though I thought the unownedExecutor on Actor was mainly so actors could specify an executor, so I'm curious to know when being able to fetch it as a SerialExecutor would be useful.
Ah, maybe that's less obvious than I thought. Yes, all of the above.
I think Konrad had actually already asked me to add that and I appear to have forgotten; it's only a couple of lines of code as I already have the bit we need, so yes, we can.
No objections from me on either front.
Yes. It's like a run loop source or a Dispatch source; the idea was to abstract things so that libraries can plug in to a RunLoopExecutor without having to know much about the underlying implementation.
That is the problem here; MainActor.executor is of type any MainExecutor. If it's a typealias, then that's any RunLoopExecutor & SerialExecutor & EventableExecutor, which isn't something Embedded Swift supports.
That said, Embedded also doesn't support MainActor right now, so maybe it doesn't matter so much.
Oh, that is much nicer. Thanks
We do want to support arbitrary Clocks here, yes. Part of the point here is that you can make your own e.g. UTCClock and then use it with Task.sleep(). Executors will very likely be constrained in how they can actually sleep, but that doesn't stop us from converting the timestamps or delays to a clock they know how to sleep on.
I think that was the status quo when I started anyway (class constrained Executors). I'll admit that I was a little surprised myself to find it that way.
Hmm, I wouldn't expect Embedded Swift to support an any MainExecutor type regardless of whether MainExecutor were an alias for a protocol composition or a protocol in and of itself, unless (as you noted) Executor was class-constrained. On the other hand, if Executor is in fact already class-constrained, then any RunLoopExecutor & SerialExecutor & EventableExecutor is also class-constrained, and Embedded Swift should support that composition as an existential. Still sounds like a bug either way to me. (If you think that MainExecutor stands on its own as a separate protocol more than the sum of its current parts, the priority of the bug may not be that high to you, of course.)
This looks very interesting Alastair, and I'm pleased to see it arriving. I expect that SwiftNIO will adopt these interfaces generally: we have users using our current terrifying hook to take over the entire concurrency pool, and many users have been sufficiently happy with the result that we'd like to take advantage of something more formal.
My only editorial note is, like @FranzBusch, it'd be useful to see an editorial section of this document that covers how you envision EventableExecutor being used. Because of the associated type being defined by the executor itself it's not immediately apparent to me what a likely choice for that type would be, so I'd love to get a little more detail on how you envision this working, even if it's just a silly PoC.
Otherwise, my only other question is whether we can clarify how the main executor flow will work. I think reading between the lines I can see it: it looks like you propose to have _swift_task_asyncMainDrainQueue become amended to call into the run method on the MainExecutor that is present for the system, but the document doesn't actually express that.
While this is the primary use-case I have seen code that accessed the unowned executor of an actor to do some thread based checking. My current understanding is that this property is unowned because we cannot safely express the lifetime constraints here. Since we are getting more lifetime features in the language it might be reasonable to have a var executor on the actor. I think @ktoso has more knowledge about this though and I would like him to chime in on this.
This is related to the above. Is it actually safe to return an actors executor here? What if you retain the executor longer than the actor's lifetime? I recall that there were some special runtime mechanics around creating and retain custom actor executors.
Great revision! This is really shaping up excellent!
This one probably needs more clarification, I agree.
Is this the "the one that owns the thread i'm running on" because we can be "on" a serial executor (isoalted by it) and "on" a task executor. Runtime has "active executor" terminology for that. Which one are we talking about here - it sounds like "the one who owns the thread" which could be the task executor.
Default actor executors (without a custom executor) are somewhat weird and special and I'm not sure how to safely expose them like that... I don't think we retain the "executor" of those in any way, because it is actually the actor itself and we just mark them as "is default" so keeping the executor would have to keep the actor object alive...? @John_McCall may remember the details of those a bit more, I'm a bit hazy on the specialness of default executors but I'm a bit worried about if we can just return one like that as any SerialExecutor - there's no such object today in theory...
So if this can return a default actor's executor, as it probably can, are we worried about some lifetime safety...? Default actor executors have some weird semantics, we don't actually retain the executor per se etc. Might be worth checking this API definitely is safe
Also because you would not want to refcount an executor every single time some enqueue happens. The executors are required to, using some other means, stay alive as long as the actors that use them do. The default actors do this by the executor being "the actor".
I don't think we'll get away from Unowned...Executors in this proposal, no.
Should run(until:) be named runUntil(_:) or similar? When using trailing closure syntax, runLoop.run { result == .finished } doesnβt clearly state what the closure is for.
Is there an expectation for the size of the pointer provided by withUnsafeExecutorPrivateData? What is its lifetime?
Should there be a var executorPrivateData: MutableRawSpan { get } property? (no, because MutableRawSpan must be fully initialized, I think?)
Is there a reason the LocalAllocator methods take a T instead of a T: ~Copyable?
I'd love the proposal, itβs a significant improvement over the existing hook-based mechanism!
Regarding the RunLoopExecutor, in environments where the event loop is fully controlled by the platform, such as JavaScript, making RunLoopExecutor a required conformance for MainExecutor introduces some challenges.
In JavaScript + Wasm environments, user programs cannot manually drive the event loop, so as a workaround, JavaScriptKit using a hack where _swift_task_asyncMainDrainQueue_hook throws a JavaScript exception (not a Swift Error) to unwind to the JavaScript call frame, avoiding hitting unreachable (generated from Never result type) without entering a run loop.
Would it be possible to make RunLoopExecutor optional for MainExecutor, and if the user specified main executor does not conform to RunLoopExecutor, simply avoid calling swift_task_asyncMainDrainQueue() in async main? This would allow event-loop-driven platforms like JavaScript to integrate more naturally without requiring unnecessary hooks.
Interesting point. That might indeed be a better idea because of the trailing closure syntax.
You get two machine words; it's part of the underlying Job structure, so it lives as long as the job/task does. If you need more than that, you could use the task allocator to allocate some space and then store the pointer to that space in the private data area.
I looked at spans before but they aren't quite fully baked yet. We can potentially add a span accessor later if it makes sense.
No. Good point.
No specific reason. I guess unsigned probably does make more sense.
Ah. In the case of default actor executors, it will return nil, since there really isn't a SerialExecutor in that case.
The original use-case for this was to allow us to pick the correct executor to yield or sleep on, and in that case this behaviour is fine, since an actor default executor is extremely unlikely to implement the delayed enqueue methods.
To clarify the precise behaviour, the current implementation returns the first of:
The preferred task executor of the current task.
The active SerialExecutor, if any (in the case of an actor default executor, there won't be one).
The current task executor.
If none of those are set, it will return nil. It makes no attempt to synthesise an executor for the actor default case, nor does it fall back to the default executor on its own. Do we think this is the correct behaviour?
Franz also asked me elsewhere which executor gets used by Task.sleep() if some of the above don't support scheduling. I had originally been thinking that most likely all the executors would, or none of them would, so this is a good point; I think we have two options β either we do the same kind of thing as above, but ignore executors that can't do scheduling, or we take the executor returned by the above, and if that can't do scheduling then fall back to the default global executor. My guess is that people would prefer the former?
Ok that makes sense; I was worried how we'd implement this for default actors and the answer is just that we don't huh. I wonder if that's confusing though... Maybe the name of the property should indicate this somehow? I mean it's not like there "isn't" anything running (executing) the current actor on such actors
Could be semantically weird to return nil when we're on a default actor? Though I agree we can't return anything else, maybe just a naming discussion of the property then.
I'm a bit confused still, let's check two cases:
I'm in a default actor method, there is an executor preference active
running on: the preferred executor
isolated by: the actor
this returns... "the preferred executor"?
in custom executor actor method, there is an executor preference set
running on: the custom executor
isolated by: the custom executor
there is a task executor preference though (!)
this returns... I would expect it to return the custom executor? but that's now how I read your 3 points, can you clarify that case?
I think I'm reaching for wording this as "active" or "effective" executor and it's "the one we're running on" though that gets confusing in the default actor case with it being nil, someone could think we're nonisolated if that returned nil
I wonder whether the issue here is really that having the entry point be async main doesn't make much sense in such an environment β on most platforms, returning from main will cause the program to terminate, but that wouldn't be true for your Javascript example.
(Before anyone points it out, I'm aware that the situation on Windows is⦠complicated. It does generally exit when you return from C's main though.)
Yeah, I think either this, or we don't make it a single property since we'll have to decide such questions which can be answered either way. Perhaps this needs to not take into account task executor and we have some currentTaskExecutor and currentSerialExecutor properties?
We need to decide the answer to this in order to make Task.sleep() and Task.yield() have sensible behaviour. Right now they unnecessarily hop executor to the global default executor, with all the attendant context switches that causes.
I have just implemented a feature to detect "the new API was implemented" in conformance flags. I think we could totally extend this to be a bit that indicates that there's extra metadata and we could use a wide range of flags to detect such things.
We could this way detect "this executor HAS IMPLEMENTED" these scheduling methods so we can use them, without having to call out into Swift to ask it "can you do this?".
Perhaps we should use that mechanism and remove the supportsScheduling API?