Consider the following where I want to use TaskGroup
to perform a series of tasks concurrency, and then want to collate the results:
func loadObjects1() async -> [Int: Foo] {
await withTaskGroup(of: (index: Int, foo: Foo?).self) { group in
for index in 0 ..< 10 {
group.addTask {
(index, try? await foo(for: index))
}
}
var values: [Int: Foo] = [:]
for await result in group where result.foo != nil { // this is fine
values[result.index] = result.foo
}
return values
}
}
Historically, I have generally simplified the last four lines with reduce
:
func loadObjects2() async -> [Int: Foo] {
await withTaskGroup(of: (index: Int, foo: Foo?).self) { group in
for index in 0 ..< 10 {
group.addTask {
(index, try? await foo(for: index))
}
}
return await group.reduce(into: [:]) { $0[$1.index] = $1.foo } // Sending main actor-isolated 'group' to nonisolated callee risks causing data races between nonisolated and main actor-isolated uses
}
}
But with Xcode 16.2, if that function is declared within an actor-isolated type, it produces an warning in Swift 5 with strict concurrency checking turned on (and it is an error in Swift 6 mode):
Sending main actor-isolated 'group' to nonisolated callee risks causing data races between nonisolated and main actor-isolated uses
Now, I obviously can accumulate my results manually like I did in loadObjects1
. Or, the problem goes away if I make this function nonisolated
, as I have in loadObjects3
, below:
nonisolated func loadObjects3() async -> [Int: Foo] {
await withTaskGroup(of: (index: Int, foo: Foo?).self) { group in
for index in 0 ..< 10 {
group.addTask {
(index, try? await foo(for: index))
}
}
return await group.reduce(into: [:]) { $0[$1.index] = $1.foo } // now OK
}
}
So, clearly, I have at least two workarounds; either make the function nonisolated
or just manually accumulate the results. But, it just feels wrong. It seems that reduce
should do the job, irrespective of the isolation of the method.
Is this a mistake in the reduce
implementation? Or is this really the status quo?
FWIW, the Foo
type and the foo(for:)
implementation are really irrelevant to the question, but for the sake of a minimal and reproducible example, here they are:
struct Foo: Decodable, Sendable {
let bar: String
}
nonisolated func foo(for index: Int) async throws -> Foo {
try await Task.sleep(for: .seconds(2))
return Foo(bar: "\(index)")
}