Is there a broken invariant here?

I think we are a little bit short on examples of actors which contain references to other actors (or maybe I just couldn't find them, maybe it's an anti-pattern?). Anyway, this sort of thing tends to lead to async methods within an actor, which tends to lead to broken invariants. I have a short example below, and I'm not sure if there is a broken invariant or not:

actor Foo {
    
    var number: Int
    
    init(_ number: Int) {
        self.number = number
    }
    
    func setNumber(to newValue: Int) {
        number = newValue
    }
}

actor Bar {
    
    var fooA = Foo(10)
    var fooB = Foo(5)
    
    // The invariant I am trying to preserve here is that fooA.number 
    // and fooB.number 
    // should not be able to change after we have fetched 
    // either fooA.number of fooB.number 
    // to calculate the sum.  In this form, I think it is clear that that invariant is broken.  
    // fooB.number can change after we have fetched fooA.number.
    func sum() async -> Int {
        
        let numberA = await fooA.number // Possible suspension point.
        let numberB = await fooB.number // Possible suspension point.
        
        return numberA + numberB
    }
    
    // Is this form semantically different from the form above?  Is there truly only one suspension point here?  If so I think my invariant is preserved?
    func alternateSum() async -> Int {
        await fooA.number + fooB.number // Only one suspension point?
    }
    
    // Assume that there are methods to set fooA and fooB.
}

Granted a trivial solution in this case is to simply create an actor that holds two Ints, and calculate the sum within that actor, but I have a more complex scenario than the example above where I don't think such a solution is viable.

Thanks for any help, I'm still struggling a little with our new actor/async/await/Task paradigm :sweat_smile:

Hi! Nice question.

IIUC, the two forms are equivalent (see below). So I don't think the invariant you want is preserved. They would need to be computed on the same actor.

Proposal 0296 says, "An await operand may contain more than one potential suspension point."

1 Like

Ah I see, thanks for clarifying!

I was looking in the wrong place over at swift-evolution/0306-actors.md at main · apple/swift-evolution · GitHub :sweat_smile:

So I guess this raises the question then:

Is an actor with references to other actors more or less an anti-pattern?

Not really, I feel like it's quite the opposite. Conceptually, the actor model proposes that actors are the "universal primitive unit of concurrent computation" – there can only be actors, and these actors are only allowed to make local decisions on their own state, create more actors, send more messages (calling actor methods in our case), and determining how to respond to the next message received.

So having references to other actors (also called "having their address") is supposedly the primary use case for actors. Now, sending messages to these other actors, which in Swift means calling on their methods / computed properties, is slightly trickier than in other actor models because they were implemented as being reentrant. This was done to avoid any possibility of deadlocks, however does introduce a whole host of complexity in terms of invariance. In the coming months and years, I trust that we'll come up with the right patterns for managing reentrancy.

1 Like

Thanks, I very much agree with your analysis. I will keep an eye out for such patterns to emerge, since currently I'm finding actors that have other actor's "addresses" to be rather tricky. I think at least a good rule of thumb I will be applying personally is to be extra careful whenever I see an await inside an actor method.

1 Like

Controlling simultaneous actor execution would be helped with custom executors and the ability to share an executor between actors. Hopefully we see that proposal sooner rather than later.

2 Likes

That's a good point, am I right in saying that if Foo and Bar in my example were actually classes tagged as @MainActor then effectively I am forcing them to share an executor, and in this case I think that would resolve the broken invariant since everything is effectively on the main thread?

(I'm not going to do that of course, but I just want to make sure I understand the implications of having them share an executor.)

In general, executing without suspension points would get the behavior you want, until we can guarantee the order of operations between actors.

1 Like