Let's say that we have the following code snippet:

import Foundation

class SomeClass {
    var count = 0
}

actor SomeActor {
    let someClass: SomeClass
    init(someClass: SomeClass) {
        self.someClass = someClass
    }
    func inc() {
        self.someClass.count += 1
    }
}

let group1 = DispatchGroup()
let group2 = DispatchGroup()

group1.enter()
group2.enter()
let someClass = SomeClass()
Task {
    let someActor = SomeActor(someClass: someClass)
    await someActor.inc()
    group1.leave()
}

Task {
    let someActor = SomeActor(someClass: someClass)
    await someActor.inc()
    group2.leave()
}

someClass.count += 1

group1.wait()
group2.wait()
print("count: "+someClass.count.description)

This doesn't cause any compile errors.

Since class types are reference-types, I guess that this might cause a data-race.

How do Actors prevent data-races for class properties in Swift?

Environment

swiftc version: 5.7.2

I don't think the language can help you here to the degree you desire.

You will need to provide safe access to the shared SomeClass object.

class SomeClass {
    var count = 0
}

actor SafeAccessSomeClass {
    private let object: SomeClass
    
    init () {
        self.object = SomeClass ()
    }
    
    func increment () {
        object.count += 1
    }
    
    func sample () -> Int {
        object.count
    }
}

@main
struct Test {
    static func main () async {
        actor SomeActor {
            let someClass: SafeAccessSomeClass
            
            init (someClass: SafeAccessSomeClass) {
                self.someClass = someClass
            }
            func inc() async {
                await someClass.increment()
            }
        }

        let range = 0..<3
        let someClass = SafeAccessSomeClass ()
        let t1 = Task {
            let someActor = SomeActor (someClass: someClass)
            for _ in range {
                await someActor.inc()
            }
        }

        let t2 = Task {
            let someActor = SomeActor (someClass: someClass)
            for _ in range {
                await someActor.inc()
            }
        }
        
        for _ in range {
            await someClass.increment()
        }

        await t1.value
        await t2.value
        print ("count: ", await someClass.sample ())
    }
}

I might be wrong here, but I look forward to hearing what the experts will say.

1 Like

Leaving aside the issue of using DispatchGroup (except as a mechanism for forcing some code on different threads, for the purposes of your question), then your code doesn't prevent data races.

  • A class instance used inside an actor is basically a "bundle" of mutable values that the actor protects. In that regard, it's no different from other mutable data that an actor might protect.

  • However, because it's a class, the protection is effective only when no references to the instance can escape from the actor, or otherwise exist outside the actor instance.

In your example, a reference to the class instance already exists outside the actor when the actor created, so the actor cannot protect the instance's data.

To have the compiler diagnose this issue, you'll need to turn on "Complete" concurrency checking in your project's build settings. In that case, you get this warning when you try to create the actors inside each of the Tasks:

Non-sendable type 'SomeClass' passed in call to nonisolated initializer 'init(someClass:)' cannot cross actor boundary

In other words, the compiler knows that there's already a reference to a SomeClass when the actor is initialized, so it knows that the actor can't fully protect access to SomeClasss mutable state.

  • The only safe way to have the actor protect data in a class instance is to create the class instance inside the actor.

Note that this entire set of answers would be different if SomeClass was Sendable, which in this context would mean, roughly, "only allows modification of its internal state in a thread-safe way" @ibex10's example illustrates one way of approaching that, but in practice creating a second actor isn't likely to be the most natural solution (unless you really want two independent and unrelated actors). The most natural solution would be to create the SomeClass instance in the actor's initializer, and let SomeActor mediate all access to SomeClass's state.

1 Like

The main thing that prevents data races is the data isolation provided by Sendable checking. Actors are one tool (among many possible) for allowing tasks to coordinate despite isolation.

1 Like