Yeah that would work. Non isolated functions like that should work fine. We’d likely want to keep the executor versions tho, move them onto SerialExecutor then?
Sure. They could even have the same names.
I guess that makes sense. It is actually right to say a serial executor "isolates" some work after all, huh. Thanks for the input!
I'll go ahead and prepare those renames/moves, seems like a good one to do, while we wait for complete review summary of this proposal
Does it tho? I’ve always thought about isolation in terms of having access to shared state which applies to actors but not to executors? I’m afraid that using isolation to talk about executors muddles the waters and makes people think about threads and queues again.
Serial executors have a meaningful concept of isolation just from the inability to run multiple things at once, which is why they can be used to implement actors. I suppose the word "isolated" makes more sense if you're talking about isolating data, but it's not like we refuse to allow actors to have no stored properties because it would make "isolated" meaningless.
I guess I should review this given that I've been using the functionality added in this proposal for a while and find it useful and important.
We have some places that need assumeOnActorExecutor()
, and our current implementation is along the lines of the following:
private extension Actor {
func check() -> (@Sendable () -> Void) {
let fn: () -> Void = { _ = self }
return unsafeBitCast(fn, to: (@Sendable () -> Void).self)
}
}
private struct ActorExecutorHelper<A: Actor> {
let actor: A
let check: @Sendable () -> Void
init(_ actor: A) async {
self.actor = actor
self.check = await actor.checker()
}
func assumeOnActorExeuctor(_ operation: (isolated A) throws -> T) rethrows -> T {
check()
try withoutActuallyEscaping(operation) { fn in
try unsafeBitCast(fn, to: ((A) throws -> T).self)(actor)
}
}
}
This is obviously gross code that should not be encouraged, and the precondition check relies on -enable-actor-data-race-checks
and so doesn't work with SPM.
I like the actor.assertIsolated()
name better than the one originally proposed.
I've found custom executors to be basically mandatory for testing code using swift concurrency. There's just no other way to test all of the various sequencings of tasks. Currently doing this requires doing naughty things because not enough is publicly exposed, so it'll be good to have a public version. I don't really have any comments on the executor API beyond that it works for the things I've tried to do so far.
The bits which aren't "this proposal is exactly the thing I want":
Ideally we'd like try await Realm()
to produce a Realm instance confined to the current actor or executor similar to how try Realm()
produces one confined to the current thread. I think with this proposal this is possible†, but awkward and (probably) suboptimal. I'm under the impression that not providing a way to get the current actor or executor is an unstated explicit design goal, but having it be possible but require jumping through hoops is a bad place to be.
This is probably just not related to custom executors at all, but the major awkward bit I keep running into is wanting to take a non-Sendable argument to an async function, and then copy it or convert it to a Sendable form before the actual async part. Currently this requires @_unsafeInheritExecutor
, so, well, that's getting slathered all over the place.
† The rough idea I have is to provide a custom executor which runs jobs on a dispatch queue and stashes a reference to the actor in the dispatch-local state. This would be somewhat awkward for users to use (they'd have to explicitly make their actors use this executor) and presumably would perform worse than the built-in executors.
Thanks for the feedback, everyone! The Language Workgroup has decided to return this proposal for some small revisions.