Sendable async closures and nonisolated(nonsending) behavior

Environment:
Swift 6.3.2, Swift 6 Language Mode, Default Actor Isolation (nonisolated), NonisolatedNonsendingByDefault enabled

It’s been my understanding that @Sendable closures are inferred to be nonisolated. That said, the following code yields an unexpected result: the @Sendable closure inherits MainActor isolation.

func callSendableClosure(_ closure: @Sendable () async -> Void) async {
    let isolation = #isolation
    print("\(isolation, default: "nonisolated")") // Swift.MainActor

    await closure()
}

@main
struct ClosureIsolation {
    // implicitly isolated to @MainActor
    static func main() async {
        await callSendableClosure {
            let isolation = #isolation
            print("\(isolation, default: "nonisolated")") // Swift.MainActor
        }
    }
}

This behavior isn’t explicitly called out in SE-0461, although it’s possible it was implied or I simply misread something. Can someone kindly confirm if this is intended behavior?

Interestingly enough, disabling NonisolatedNonsendingByDefault and explicitly marking the function nonisolated(nonsending) results in the behavior I originally expected: the @Sendable closure executes on the GCE.

nonisolated(nonsending)
func callSendableClosure(_ closure: @Sendable () async -> Void) async {
    let isolation = #isolation
    print("\(isolation, default: "nonisolated")") // Swift.MainActor

    await closure()
}

@main
struct ClosureIsolation {
    // implicitly isolated to @MainActor
    static func main() async {
        await callSendableClosure {
            let isolation = #isolation
            print("\(isolation, default: "nonisolated")") // nonisolated
        }
    }
}
1 Like

When enabling NonisolatedNonsendingByDefault it applies nonisolated(nonsending) to both the method and the closure parameter. So the method signature is this:

nonisolated(nonsending) func callSendableClosure(_ closure: nonisolated(nonsending)  @Sendable () async -> Void) async

A closure that is nonisolated(nonsending) has the same semantics as a method i.e. it inherits the callers isolation. A nonisolated(nonsending) @Sendable closure also inherits the callers isolation but due to it being @Sendable it can also be called from multiple isolation contexts concurrently e.g. if you were to call the closure from multiple child tasks concurrently.

4 Likes

Ah! I wasn't aware the closure implicitly becomes nonisolated(nonsending) too. Thanks for the clarification @FranzBusch.