1

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.

1 Answer 1

2

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!
Sign up to request clarification or add additional context in comments.

1 Comment

Ah, this "A closure expression is inferred to be @Sendable if either" clears is up. Excellent!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.