I have a situation where I think it makes sense to have a non-Sendable actor and was wondering if this sounds crazy.
The fundamental thing I'm trying to achieve is the ability to use a manual executor to step through the suspension points in a task. So something like the following:
final class ManualExecutor: SerialExecutor {
private nonisolated(unsafe) var jobs = UniqueArray<ExecutorJob>()
func enqueue(_ job: consuming ExecutorJob) {
jobs.append(job)
}
func runNext() -> Bool {
guard let job = jobs.first else {
return false
}
jobs.removeFirst()
job.runSynchoronously(on: asUnownedSerialExecutor)
return true
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
actor Foo {
var x: Int = 0
func foo() {
Task {
await doStuff()
x += 1
await doStuff()
x += 1
await doStuff()
x += 1
}
}
let manualExecutor: ManualExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
manualExecutor.asUnownedSerialExecutor()
}
}
let foo = Foo()
foo.foo()
while foo.manualExecutor.runNext() {
print(foo.x)
}
A few questions:
- Does this make sense?
- Is there an easier way to do this?
- Can I avoid needing to lock inside of
ManualExecutorif I never sendFoo(which I'm enforcing by storingFooin a non-sendable struct, and being careful to not hop to a different executor). - How should I think about the performance implications of this pattern versus implementing the state machine manually without actors + structured concurrency?
- Is there a way to detect/enforce that you can't hop off of this executor?