Swift Concurrency still allowing data race when using closures

Hello all, I'm trying to figure out if the following is a bug or not related to the Swift6 language mode & structured concurrency. It seems that adding an @MainActor annotation to a method assumes that closures used within that method are also @MainActor even though they may not be. This came up when migrating my codebase to the Swift6 language mode using Xcode 16 betas.

Given a class that looks like this:

class Foo {
  func bar() {
    SomeOtherType(onBaz: { [weak self] in
        self?.baz()
    })
  }

  @MainActor
  func baz() {
    ...
  }
}

The compiler will give us the proper error, Call to main actor-isolated instance method 'baz()' in a synchronous nonisolated context. This makes sense, because baz() is marked as @MainActor. However, adding @MainActor to bar() will resolve the error even though the closure onBaz may not be called on the MainActor. Here is an example where you can mutate state on both a background task and on the main actor possibly causing a data race:

import Foundation

class Foo {

    var mutableState: Int = 0

    @MainActor
    func bar() {
        SomeOtherType { [weak self] in
            // mutates state from bg task:
            self?.baz()
        }

        Task {
            // mutate state from mainactor:
            mutableState = 20
        }
    }

    @MainActor func baz() {
        print(Thread.isMainThread) // false
        mutableState = 10
    }
}

class SomeOtherType {
    init(onBaz: @escaping () -> Void) {
        Task { // background task...
            onBaz()
        }
    }
}

let foo = Foo()
Task { @MainActor in
    foo.bar()
}

Is this a bug? Is Foo being assumed Sendable when it is not? This happens regardless of if I annotate mutableState with @MainActor.

2 Likes

This won't compile in Swift 6 mode, as onBaz should be @Sendable. Marking it so will trigger chain of errors, resolve which would be possible my making Foo conform to Sendable.

Ah! You're right— when using this in a project it doesn't compile, that's good. I incorrectly assumed that Playgrounds in Xcode 16 would default to the Swift6 language mode, but it appears they're created with Swift5 still. I hope that is updated in a future beta.

Thanks @vns !

2 Likes