SE-0461 clarifies that (non-sending, non-@Sendable) async closures inherit the actor isolation where they’re declared:
If the contextual type of the closure is neither
@Sendablenorsending, the inferred isolation of the closure is the same as the enclosing context
If the isolation is held by the closure value itself, why can’t that closure be sent to another isolation domain?
func sendToNonIsolated(_ operation: @escaping () async -> Void) {
// Crosses isolation boundary
Task.detached {
await operation() // ERROR
}
}
I understand the closure is not marked as @Sendable, but if it were sent and called on another isolation unsafely, the closure still holds onto its isolation and is called on that original isolation, not the isolation where it’s being called:
func sendToNonIsolated(_ operation: UncheckedClosure) {
Task.detached {
// This task is running in a nonisolated context, but operation.value
// is still isolated to the TestActor.
await operation.value()
}
}
struct UncheckedClosure: @unchecked Sendable {
let value: () async -> Void
}
final actor TestActor {
var count: Int = 0
func send() {
let unchecked = UncheckedClosure {
self.count += 1 // Ensures this is not a `@Sendable` closure
print(#isolation) // Prints TestActor
}
sendToNonIsolated(unchecked)
}
}
Even though the closure was sent to another isolation domain using @unchecked Sendable, it’s still being called on its original isolation, implying the await to call it is actually performing an extra actor hop to get back onto the correct isolation before running the closure in that isolation.
If an async closure inherits its actor context, and takes the necessary steps to ensure it’s called on that actor, even when forcefully sent to another isolation context, why are any async closures non-@Sendable? It seems like no matter which isolation context it’s called in, it will always wait for the original isolation to be available and run on that isolation, which makes it safe to call from any isolation since the closure is implicitly isolated. Are there other cases I’m not considering?
I understand I can just mark the closure as @MainActor or use some other global actor to ensure its isolation information is held onto with its type information, but for writing an API where I don’t know the intended isolation, why can’t I take in a regular async closure (non-sending, non-@Sendable) and know that it can be called from any isolation?