Data races with async/await and Strict Concurrency Checking = Complete

The crash below is 100% reproducible for me. My assumption was that with Strict Concurrency Checking set to Complete this shouldn't compile because Repository isn't Sendable. Is this a known bug?

Code
import SwiftUI

@main
struct AsyncAwaitWithoutActorApp: App {
    @ObservedObject
    var viewModel = ViewModel()

    var body: some Scene {
        WindowGroup {
            Button("Start") {
                Task {
                    await viewModel.run()
                }
            }
        }
    }
}

class Repository {
    var results: [Int] = []

    func add(value: Int) {
        results.append(value)
    }
}

@MainActor
class ViewModel: ObservableObject {
    var repository = Repository()

    func run() async {
        for value in 0..<1000 {
            Task {
                await Operation(repository: self.repository).execute(value: value)
            }
        }
    }
}

struct Operation {
    let repository: Repository

    func execute(value: Int) async {
        repository.add(value: value)
    }
}
1 Like

Thinking further about this, is it even possible to prevent data races like this at compile time if async member functions on non-sendable, non-actor-bound types are allowed in the language?

This is a known hole in Sendable checking right now. Sendable checking hole when actors call non-mutating async methods on non-Sendable types · Issue #65315 · apple/swift · GitHub

The problem is that any non actor isolated async method is running on the global concurrency pool. This means that any async method requires all its captures to be Sendable. The compiler is currently not diagnosing this.

You can make this code safe by making Repository an actor so that its mutable state is protected.

2 Likes

Interesting, thanks.

Yes I know, I was just wondering why the compiler wouldn't complain about it.

As a rule of thumb, to prevent data races, it looks like that all types that have async member functions should either be Sendable, or be bound to an actor, or be an actor themselves, right?

Correct.