The Swift 6 compiler emits the following error
Sending value of non-Sendable type '() async throws -> ()' risks causing data races
when explicitly specifying an actor as the isolation for the closure in the addTask
function of the task group:
func test() async throws {
try await withThrowingTaskGroup { group in
group.addTask { @MainActor in // <== Error
try await Task.sleep(nanoseconds: 1_000_000)
}
try await group.waitForAll()
}
}
I don't see why the closure should not be sendable. Why is the compiler considering the given closure as not sendable?
Info: the signature of addTask
is
mutating func addTask(
priority: TaskPriority? = nil,
operation: sending @escaping @isolated(any) () async throws -> ChildTaskResult
)
What I'm trying to do is the following (where the compiler issues the same error):
@Sendable
func requiresIsolation(isolated: isolated any Actor = #isolation) async throws {}
(this function is sendable, so @Sendable
is redundant and just for emphasising the fact)
and then:
func test() async throws {
try await withThrowingTaskGroup { group in
group.addTask { @SomeActor in
try await requiresIsolation()
}
try await group.waitForAll()
}
}
Update:
My suspicion is, that the closure needs an explicit @Sendable
annotation.
The following code makes the compiler happy:
func test() async throws {
try await withThrowingTaskGroup { group in
group.addTask { @Sendable @MainActor in // <== add `@Sendable`
try await requiresIsolation()
}
try await group.waitForAll()
}
}
Still, I'm a bit unsure whether this is correct.
Adding @Sendable
is a correct solution to this. Swift simply fails to infer the sendability of the closure because the expected type is sending
, not a @Sendable
closure. See the SE proposal for when a closure is inferred to be @Sendable
:
A closure expression is inferred to be
@Sendable
if either:
- it is used in a context that expects a
@Sendable
function type or@Sendable
is in the closure's in specification.
addTask
does not expect a @Sendable () async throws -> Void
, but a sending () async throws -> Void
.
If addTask
had been (and this is a stronger requirement than sending
):
func addTask(
priority: TaskPriority? = nil,
operation: @Sendable @escaping @isolated(any) () async throws -> Void
)
then the closure will be inferred to be Sendable
.
In fact, all actor-isolated async
closures are Sendable
, since the caller must await
to call it, and so an actor hop can be performed.
The problem with { @MainActor in try await ... }
is that the compiler does not infer the type of the closure as a @MainActor () async throws -> Void
, but just a value of type () async throws -> Void
with its isolation region being the main actor. This isolation region is what's stopping you from sending
it, as per region-based isolation rules.
If you specify its type to be the desired actor-isolated type, it becomes sendable:
let c: @MainActor () async throws -> Void = { /*@MainActor here is redundant*/
try await Task.sleep(nanoseconds: 1_000_000)
}
let d: any Sendable = c // no error here!
group.addTask(operation: c) // no error here!