swift icon indicating copy to clipboard operation
swift copied to clipboard

TaskGroup can't be used in GlobalActor isolated context in Swift 6

Open nh7a opened this issue 1 year ago • 3 comments

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

nh7a avatar Jun 27 '24 01:06 nh7a

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.

moreindirection avatar Jul 05 '24 15:07 moreindirection

Possibly related to #72942

moreindirection avatar Jul 05 '24 19:07 moreindirection

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) {
            
        }
    }
}

pbk20191 avatar Jul 10 '24 01:07 pbk20191

I believe @gottesmm or @ktoso fixed this recently

hborla avatar Jul 14 '24 02:07 hborla

Yeah I think this had to do with improving how we treat the async closure and adopting isolation. I can double check!

ktoso avatar Jul 14 '24 02:07 ktoso

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".

ktoso avatar Jul 17 '25 02:07 ktoso

rdar://133358159

ktoso avatar Jul 17 '25 02:07 ktoso