Regression in Concurrent Task Execution on macOS 15 Beta: Seeking Clarification

Developer Community,

I've noticed a significant change in concurrent task execution behavior when testing on macOS 15 beta 4 & Xcode 16 Beta 4 compared to previous versions. Tasks that previously ran concurrently now appear to execute sequentially, impacting performance and potentially affecting apps relying on concurrent execution.

To illustrate this, I've created a simple toy example:

import SwiftUI

struct ContentView: View {
    @State private var results: [String] = []

    var body: some View {
        VStack {
            Button("Run Concurrent Tasks") {
                results.removeAll()
                runTasks()
            }
            ForEach(results, id: \.self) { result in
                Text(result)
            }
        }
    }

    func runTasks() {
        Task {
            async let task1 = countingTask(name: "Task 1", target: 1000)
            async let task2 = countingTask(name: "Task 2", target: 5000)
            async let task3 = countingTask(name: "Task 3", target: 1500)

            let allResults = await [task1, task2, task3]
            results = allResults
        }
    }

    func countingTask(name: String, target: Int) async -> String {
        print("\(name) started")
        var count = 0
        for _ in 0..<target {
            count += 1
        }
        print("\(name) finished. Count: \(count)")
        return "\(name) completed. Count: \(count)"
    }
}

Observed behavior (macOS 15 Beta 4 & Xcode 16 Beta 4):

Tasks appear to execute sequentially:

Task 1 started
Task 1 finished. Count: 1000
Task 2 started
Task 2 finished. Count: 5000
Task 3 started
Task 3 finished. Count: 1500

Expected behavior:

Tasks start almost simultaneously and finish based on their workload:

Task 1 started
Task 2 started
Task 3 started
Task 1 finished. Count: 1000
Task 3 finished. Count: 1500
Task 2 finished. Count: 5000

Observed behavior in macOS 15 Beta:

The profile reveals that the tasks are executing sequentially. This is evidenced by each task starting only after the previous one has completed.

Answered by DTS Engineer in 797639022

The code you’ve posted looks like it’s working as designed. Your countingTask(…) method is isolated to the main actor (more on that below) and is entirely CPU bound. Main-actor-isolated code must run on the main thread, and thus you see everything serialised.

I think the point of confusion relates to your use of SwiftUI. Prior Xcode 16 beta, SwiftUI annotated its body property as @MainActor. In Xcode 16 beta it annotates View that way. This was a sensible change, IMO, but it will result in the behaviour you’re seeing.

We did mention this multiple times at WWDC, and discussed it in the release notes (search any of our platform release notes for “120815051”), but it’s a subtle change and thus it’s easy to miss.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Concurrent Task Execution Issue in iOS 18 Beta 4 (22A5316j)

Overview

The previously reported issue of concurrent tasks executing sequentially in macOS 15 Beta has also been observed in iOS 18 Beta 4 (22A5316j). This behavior change potentially impacts apps relying on concurrent execution for performance optimization.

Observed Behavior

When running multiple asynchronous tasks that should execute concurrently, the tasks are instead running sequentially. This is evidenced by the following console output:

Task 1 started
Task 1 finished. Count: 1000
Task 2 started
Task 2 finished. Count: 5000
Task 3 started
Task 3 finished. Count: 1500

The code you’ve posted looks like it’s working as designed. Your countingTask(…) method is isolated to the main actor (more on that below) and is entirely CPU bound. Main-actor-isolated code must run on the main thread, and thus you see everything serialised.

I think the point of confusion relates to your use of SwiftUI. Prior Xcode 16 beta, SwiftUI annotated its body property as @MainActor. In Xcode 16 beta it annotates View that way. This was a sensible change, IMO, but it will result in the behaviour you’re seeing.

We did mention this multiple times at WWDC, and discussed it in the release notes (search any of our platform release notes for “120815051”), but it’s a subtle change and thus it’s easy to miss.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

To use async/await from SwiftUI it is .task not Task and to get a background thread for a func declared inside the View struct (which is annotated MainActor) use nonisolated func, e.g.

          Button(isRunning ? "Stop" : "Run Concurrent Tasks") {
                results.removeAll()
                isRunning.toggle()
          }
          .task(id: isRunning) {
               // main thread
               if isRunning {
                   async let task1 = countingTask(name: "Task 1", target: 1000)
               }
          }
          ...
    }

    // without nonisolated it would be main actor and thus always on main thread
    func nonisolated countingTask(name: String, target: Int) async -> String {
         // background thread

If you moved the func to a custom struct then it wouldn't need nonisolated. If that controller struct was an EnvironmentKey then it could be mocked for Previews.

Regression in Concurrent Task Execution on macOS 15 Beta: Seeking Clarification
 
 
Q