TaskGroup can't be used in GlobalActor isolated context in Swift 6
Description
TaskGroup can't be used in a GlobalActor isolated context (e.g. MainActor) because "nonisolated callee risks causing data races."
Reproduction
The code below doesn't build with Swift 6. It builds if @MainActor is removed.
@MainActor
func foobar() async throws {
await withTaskGroup(of: Void.self) { group in
// 🔴 Sending main actor-isolated 'group' to nonisolated callee risks
// causing data races between nonisolated and main actor-isolated uses
_ = await group.reduce(into: [Int]()) { _, _ in }
// 🔴 Sending main actor-isolated '$generator' to nonisolated callee risks
// causing data races between nonisolated and main actor-isolated uses
for await _ in group {}
}
}
Expected behavior
I expect this builds without errors in Swift 6.
Environment
swift-driver version: 1.110 Apple Swift version 6.0 (swiftlang-6.0.0.4.52 clang-1600.0.21.1.3)
Additional information
No response
I'm experiencing this too. Seems to be any @MainActor-isolated class that uses withTaskGroup and does a for await _ in group { ... }. I tried to isolate it into a sample project but couldn't reproduce it outside the context of my full app.
Possibly related to #72942
for now you can use TaskGroup's own backdeployed API available on swift6
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@frozen public struct TaskGroup<ChildTaskResult> where ChildTaskResult : Sendable {
/// - Returns: The value returned by the next child task that completes.
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@backDeployed(before: macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0)
public mutating func next(isolation: isolated (any Actor)? = #isolation) async -> ChildTaskResult?
/// Wait for all of the group's remaining tasks to complete.
public mutating func waitForAll(isolation: isolated (any Actor)? = #isolation) async
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@frozen public struct ThrowingTaskGroup<ChildTaskResult, Failure> where ChildTaskResult : Sendable, Failure : Error {
public mutating func waitForAll(isolation: isolated (any Actor)? = #isolation) async throws
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
@backDeployed(before: macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0)
public mutating func next(isolation: isolated (any Actor)? = #isolation) async throws -> ChildTaskResult?
public mutating func nextResult(isolation: isolated (any Actor)? = #isolation) async -> Result<ChildTaskResult, Failure>?
}
@MainActor
func foobar() async throws {
await withTaskGroup(of: Void.self) { group in
_ = await group.waitForAll(isolation: #isolation)
while let value = await group.next(isolation: #isolation) {
}
}
}
I believe @gottesmm or @ktoso fixed this recently
Yeah I think this had to do with improving how we treat the async closure and adopting isolation. I can double check!
The problem here is the reduce() function which does not take caller isolation, so it seems it is sending the group. We have to fix in general how all asynchronous sequence APIs work. You can just use next() in the meantime or iterate over the group using for el in group but the group.reduce() won't work because, as you can see below, it does not take #isolation:
public func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult:
(_ partialResult: Result, Element) async throws -> Result
) async rethrows -> Result {
in the post-6.2 world, the fix will be to make all these things nonisolated(nonsending) which is "take the caller isolation".
rdar://133358159