swiftconcurrencyactorswift-concurrency

Access global actor property from its shared instance


I'm learning about the new concurrency model that Swift offers with its actor mechanism and I'm having a hard time figuring out the concrete use of a globalActor (except from @MainActor).

Given this example:

@globalActor
actor TestGlobalActor {
    static var shared = TestGlobalActor()
    
    var data = [1, 2, 3]
}

@TestGlobalActor
class TestClass {
    func handleData() {
        print(TestGlobalActor.shared.data) // 🛑 Actor-isolated property 'data' can not be referenced from global actor 'TestGlobalActor'
    }
}
  1. why cannot I access the data property of my global actor given the fact that the method handleData is supposed to perform on TestGlobalActor

  2. Is a global actor supposed to be used in this way, by adding methods and variables in it? or is it just a standalone piece of boilerplate code that is not meant to be extended?

I was able to get the result I expected the above code to produce, but it requiered me to add another inbetween singleton class:

@globalActor
actor TestGlobalActor {
    static var shared = TestGlobalActor()
}

@TestGlobalActor
class Singleton {
    static var shared = Singleton()
    private init() {}
    
    var data = [1, 2, 3]
}

@TestGlobalActor
class TestClass {
    func handleDate() {
        print(Singleton.shared.data) // ✅
    }
}

Solution

  • By annotating something with a global actor, that thing is isolated to the shared instance of the global actor. But there could be other instances of the actor!

    So just by declaring shared as a var, the code is not really safe. shared could easily be set to some other instance later down the line, and I don't know what will happen after that. Accessing the var shared will give you a warning if you use the -warn-concurrency option.

    Let's make that a let:

    @globalActor
    actor TestGlobalActor {
        static let shared = TestGlobalActor()
        
        var data = [1, 2, 3]
    }
    

    But it is still not safe accessing data from handleData. Swift isn't smart enough to see that this access of data accesses the same instance of TestGlobalActor that is used when you use it as a global actor.

    In other words, as far as actor isolation is concerned, TestGlobalActor.shared.data "looks the same" as TestGlobalActor().data. In either case, Swift thinks that the instance on which data is accessed is not the same instance as the global actor.

    What you can do is mark data as @TestGlobalActor too:

    @globalActor
    actor TestGlobalActor {
        static let shared = TestGlobalActor()
        
        @TestGlobalActor
        var data = [1, 2, 3]
    }
    

    This makes your code compile, but it also means that the data of other TestGlobalActor instances are also isolated to the global actor, and not the individual actor instances, which is quite weird.

    Of course, if you intend on only having one instance of TestGlobalActor, then there is no problem. Write a private init() {} in there while you're at it.

    If you do intend on having multiple instances of TestGlobalActor, then using a global actor probably isn't suitable. Move data somewhere else, or move whatever uses data into TestGlobalActor.