Search code examples
swiftactorswift-concurrencyswift6xcode16

Closures in actors: Sending 'nonSendable' risks causing data races


Why is this not allowed in Swift 6 (Xcode 16 Beta 3)?

class NonSendable { }

actor MyActor {
    func foo() {
        let nonSendable = NonSendable()

        for _ in 1...3 {
            // ✅ Compiles fine
            bar(nonSendable)
        }
        
        (1...3).forEach { _ in
            // ❌ Sending 'nonSendable' risks causing data races
            // 'self'-isolated 'nonSendable' is captured by a actor-isolated
            // closure. actor-isolated uses in closure may race against later
            // nonisolated uses
            bar(nonSendable)
        }
    }
    
    func bar(_: NonSendable) { }
}

Solution

  • Swift 5.10 was overly conservative regarding passing non-Sendable types to different contexts. Specifically, we might create a non-Sendable instance, and pass it to some other context, but not use it outside of that new context. Swift 6 (specifically SE-0414) has improved this. As WWDC 2024 video What’s new in Swift says:

    To ensure safety, complete concurrency checking in Swift 5.10 banned passing all non-Sendable values across actor isolation boundaries. Swift 6 can recognize that it is safe to pass non-Sendable values, in scenarios where they can no longer be referenced from their original isolation boundary.

    So, as you noted, in Swift 6 (in Xcode 16 beta 3), you will get a warning with the following code:

    sendable warning

    In this case, though, it is the presence of the reference to nonSendable in the for-in loop that affects its isolation region. E.g., remove that reference and the error goes away:

    actor MyActor {
        func foo() {
            let nonSendable = NonSendable()
    
            // for _ in 1...3 {
            //     bar(nonSendable)
            // }
    
            (1...3).forEach { _ in
                // ✅ Compiles fine
                bar(nonSendable)
            }
        }
    
        func bar(_ object: NonSendable) { }
    }
    

    no sendable warning

    This Swift 6 behavior is an improvement over Swift 5.10. See SE-0414 – Region based Isolation for a lengthy discussion regarding what improvements Swift 6 provides and the limitations that are still imposed.


    For the sake of clarity, your original example (with both the for-in loop and the forEach closure) did not actually manifest a data race. But the question is whether the compiler can guarantee that the code is free from races: At this point it cannot.

    In terms of work-arounds, either avoid attempting to use the nonSendable instance from two different regions, or make the object Sendable.


    As of Xcode 16.2, neither Swift 5 or Swift 6 language modes will produce the error in the OP’s original code snippet. The compiler is now much smarter and can reason about the code snippet better, and realizes that there is no issue.

    So, to illustrate the broader issue, here is a contemporary example that still illustrates how region based isolation will warn you of potential races. In the following, I can send the non-Sendable object to a different asynchronous context, but only if there are no subsequent references in the original context. Consider:

    actor MyActor {
        func foo() {
            let nonSendable = NonSendable()
    
            Task.detached {
                self.bar(nonSendable)
            }
    
            // if you uncomment the following, the above will warn you of the potential race
            //
            // print(nonSendable)
        }
    
        nonisolated func bar(_ object: NonSendable) { }
    }