Does a non-Sendable actor make sense?

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 ManualExecutor if I never send Foo (which I'm enforcing by storing Foo in 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?