swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67k stars 10.32k forks source link

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

Open nh7a opened 2 weeks ago

nh7a commented 2 weeks ago

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

moreindirection commented 1 week ago

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 commented 1 week ago

Possibly related to #72942

pbk20191 commented 6 days ago

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

        }
    }
}
hborla commented 2 days ago

I believe @gottesmm or @ktoso fixed this recently

ktoso commented 2 days ago

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