How to properly use custom executor on global actor?

Hi Everyone

I am experimenting with creation of global actor MyGlobalActor with a custom serial executor that is supposedly dispatching / enqueueing tasks on a specific dispatch queue MyGlobalActorQueue.
When I try to apply the global actor to a custom service (class) type, calls to that function don't seem to use the serial executor I created, the way I checked is by putting a breakpoint on the executor enqueue function.
Am I doing something wrong, or is this a bug?

Here is my custom executor

import Dispatch

final class GlobalActorSerialExecutor: SerialExecutor {
  private let queue = DispatchQueue(label: "MyGlobalActorQueue")

  func enqueue(_ job: UnownedJob) {
    queue.async {
      job.runSynchronously(on: self.asUnownedSerialExecutor())
    }
  }

  func asUnownedSerialExecutor() -> UnownedSerialExecutor {
    return UnownedSerialExecutor(ordinary: self)
  }
}

My global actor

@globalActor actor MyGlobalActor: GlobalActor {
  static let shared = MyGlobalActor()
  static var sharedUnownedExecutor: UnownedSerialExecutor {
    GlobalActorSerialExecutor().asUnownedSerialExecutor()
  }
}

My class annotated as my global actor

@MyGlobalActor
final class MyService {
  var state = 0

  func doStuff() {
    state += 1
  }
}

and somewhere at call site

Task {
  let service = await MyService()
  await service.doStuff()
}

Thanks
Filip

That’s not how you implement custom executor for actor. You need to define

nonisolated var unownedExecutor: UnownedSerialExecutor

on actor instance type, then shared instance of an actor will use it.

You also can make static executor property in case you want to share executor between instances or allow other actors use it:

@globalActor 
actor MyGlobalActor: GlobalActor {
    static let shared = MyGlobalActor()
    static let sharedUnownedExecutor: UnownedSerialExecutor = GlobalActorSerialExecutor()
        .asUnownedSerialExecutor()

    nonisolated var unownedExecutor: UnownedSerialExecutor { 
        Self.sharedUnownedExecutor 
    }
}
7 Likes

Yep that’s the right way.

One extra note is to watch out that the actors executor lifetime extends beyond the actors lifetime — the “unowned executor” quite literarily is not owned (retained) by anything, so make sure the actor or some global thing retains it while things are supposed to be running on it

4 Likes

Thanks @vns and @ktoso

I edited @vns's snippet to actually retain the executor, otherwise I got a crash.

@globalActor
actor MyGlobalActor: GlobalActor {
  static let shared = MyGlobalActor()
  private static let executor = GlobalActorSerialExecutor()
  static let sharedUnownedExecutor: UnownedSerialExecutor = MyGlobalActor.executor
    .asUnownedSerialExecutor()

  nonisolated var unownedExecutor: UnownedSerialExecutor {
    Self.sharedUnownedExecutor
  }
}

It now works.
Thank s a lot for your help, finding info and samples on how to do this is difficult, I tried for 2 hours :)

1 Like

Thank you for the feedback, we're aware the documentation is lacking and will try to improve the situation.

Today the best resource for things is going to be the evolution proposals: swift-evolution/proposals/0392-custom-actor-executors.md at main · apple/swift-evolution · GitHub

For this specific topic I think we could potentially add a section in here SerialExecutor | Apple Developer Documentation... I'll look into it.

4 Likes

Thats a great Idea, I actually opened that documentation and expected to find a sample / programming guide on how to create one before looking on the forums.

When it comes to the swift evolution, I do look for things there quite often, but sometimes, terminology used in the swift evolution titles might not come obvious to users of swift (as opposed to ones developing it). I have no idea on how to improve that really.

BR
Filip

1 Like

I took a stab at it over here: [Concurrency] Document custom executors in API docs a bit by ktoso · Pull Request #73634 · apple/swift · GitHub -- there's more things that should be included here, like global actors etc, but that's a first stab triggered by this thread.

We'll keep improving documentation as we polish up the Swift 6 release, thank you for reminding me about this.

2 Likes

I have this dream that we’d soon be able to feed all the evolution proposals to an LLM and generate supplementary docs to TSPL…

4 Likes

… which will then mislead folks horribly )-:

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

6 Likes

Sure, thus still a dream ;-)

I guess my main point is that the proposals are a treasure trove of information that is usually not covered elsewhere in docs, it would be amazing if we could find a way to consolidate that information somehow.