Is it possible to actor-isolate a scope without using a global actor?

Currently in my app I have some code which uses an actor to do concurrent work in the background like:

@ServiceActor
final class Service { ... }

final class SomeObject {
    @ServiceActor
    let service = Service()

    @ServiceActor
    func updateService() {
        service.parameter = ...
        service.update()
    }
}

This works fine and well, but it has the downside of all instances of SomeObject using the same instance of ServiceActor to do work, which could be a bottleneck if I have many instances of SomeObject at once. Since each instance of SomeObject uses its own instance of Service, there's no need for each object instance to await while another is doing work on ServiceActor.

Ideally, Service would be an actor itself:

actor Service { ... }

However; I'm struggling to find any way to isolate work to a specific actor instance from outside the actor without using a global actor. Without that capability, my updateService has to become:

func updateService() async {
    await service.parameter = ...
    await service.update()
}

Which is problematic because it introduces the possibility of reentrancy bugs between the await calls. Ideally i'd be able to pass my actor instance like you can with global actors:

func updateService() async {
    await Task { service in
        service.parameter = ...
        service.update()
    }
}

Is there any way to isolate a method, Task, property, etc. to a specific actor instance without using global actors?

You probably want to test whether it's actually a bottleneck before you worry too much about this.

Move the work inside Service so you only need a single await from SomeObject?

You probably want to test whether it's actually a bottleneck before you worry too much about this.

Definitely something we're looking into, but also would like to know if this is even possible because it has useful applications in general.

Move the work inside Service so you only need a single await from SomeObject ?

Yes, that does seem to be an option, but is far from ideal. My example is a simple use case; in production code there would be far more inputs to Service than outlined here and it would be impractical to have to set them all each time you need Service to do work. Also for other use cases, this may not be an option.

You can make use of isolated parameters to say that a particular method takes an instance of an actor and is isolated to that instance when called. For example:

actor Service {
  func update1() {}
  func update2() {}
}


class SomeObject {
  // cleaner version
  func updateService(service: isolated Service) async {
    service.update1()
    service.update2()
  }

  // version that is a bit closer to your example
  func altUpdateService(service: Service) async {
    Task {
      await { (s: isolated Service) in
        s.update1()
        s.update2()
      }(service)
    }
  }
}

Notice that in the second version, an await is only required to call the non-async closure that has an isolated parameter, but once inside the isolated Service function, no await is required between the two update calls.

1 Like

Perfect, I think this is exactly what I was looking for! Thank you!

1 Like