To run CPU-intensive Swift calculations in parallel, give the worker an explicit @concurrent boundary and divide independent work among child tasks. This Mac example keeps interface updates on MainActor and combines synthetic results without shared mutation.
Why adding tasks can still serialize the calculation
Suppose a Mac app has a Calculate button that starts a substantial local calculation. Wrapping the calculation in Task { ... } provides an asynchronous entry point, but a task created in a main-actor context can inherit that isolation. A long synchronous loop inside it still occupies the main actor. Apple demonstrates this distinction in Embracing Swift concurrency.
Adding a task group also leaves an important question unanswered: where does the function containing the expensive loop execute? If every child calls the same @MainActor calculation method, that actor serializes the calculation. Actors permit one task at a time to access their isolated state, as explained in the Swift language guide’s Concurrency chapter.
For Swift 6.2 and later, mark the asynchronous calculation entry point @concurrent. It explicitly switches execution off the caller’s actor. With the NonisolatedNonsendingByDefault feature enabled, an ordinary nonisolated asynchronous function instead runs on the caller’s actor; older behavior differed. The implemented SE-0461 proposal documents both the feature boundary and the explicit attribute.
Two separate choices produce the desired behavior: the worker’s isolation permits execution away from MainActor, and multiple child tasks provide independently schedulable work. The runtime decides which available threads execute those tasks. Neither the task count nor @concurrent promises a particular number of simultaneously executing cores. Apple describes that scheduling model in Embracing Swift concurrency.
Define a calculation that can be divided correctly
The example below lets a Mac user start or cancel a synthetic calculation and display its final checksum. All inputs are constants or generated integers. The calculation has this exact value contract:
- Visit every integer index in
0..<countexactly once. - Convert each index to
UInt64and repeatedly applyx = (x &* 1_664_525) &+ 1_013_904_223for the requested number of rounds. - Add the resulting values using wrapping addition.
- Return zero for an empty input, reject negative counts or rounds, and reject a nonpositive worker limit.
- Throw on observed cancellation instead of presenting an incomplete checksum as a completed result.
Wrapping arithmetic is intentional here. Swift’s ordinary integer arithmetic traps on overflow; its &+ and &* operators explicitly support overflow behavior. See Advanced Operators.
For this contract, each index is independent, and wrapping addition combines partial totals modulo 2 to the power of 64. Therefore, changing the grouping or completion order preserves the exact final value. That conclusion follows from the chosen arithmetic. A floating-point calculation needs its own correctness rule because regrouping additions can change rounding.
The partition uses division and remainder: each worker receives an equal base number of indices, and the first few workers receive one extra index. For an illustrative count of 17 and three workers, the ranges are 0..<6, 6..<12, and 12..<17. They cover the input once, including the remainder.
Build the complete Mac calculation example
Use a Swift 6.2-or-later toolchain and a macOS SwiftUI app target with a deployment target of macOS 13 or later. Replace the generated app entry point and content view with the following two blocks, placed together in one Swift file. They form one complete program; keep only one @main app declaration in the target.
The calculation namespace is explicitly nonisolated, so its members do not acquire inferred global-actor isolation. Swift documents this type-level declaration in SE-0449. Both asynchronous calculation methods use @concurrent to state their execution boundary explicitly.
import Foundation
import SwiftUI
nonisolated enum CalculationError: Error {
case invalidInput
}
nonisolated enum SyntheticCalculator {
@concurrent
static func run(
count: Int,
rounds: Int,
workerLimit: Int
) async throws -> UInt64 {
guard count >= 0, rounds >= 0, workerLimit > 0 else {
throw CalculationError.invalidInput
}
try Task.checkCancellation()
guard count > 0 else { return 0 }
let workers = min(count, workerLimit)
let baseSize = count / workers
let remainder = count % workers
return try await withThrowingTaskGroup(
of: UInt64.self,
returning: UInt64.self
) { group in
var lower = 0
for worker in 0..<workers {
try Task.checkCancellation()
let size = baseSize + (worker < remainder ? 1 : 0)
let start = lower
let end = start + size
lower = end
group.addTask {
try await calculateChunk(
lower: start,
upper: end,
rounds: rounds
)
}
}
var total: UInt64 = 0
for try await partial in group {
total = total &+ partial
}
try Task.checkCancellation()
return total
}
}
@concurrent
private static func calculateChunk(
lower: Int,
upper: Int,
rounds: Int
) async throws -> UInt64 {
try Task.checkCancellation()
var total: UInt64 = 0
for index in lower..<upper {
if index % 256 == 0 {
try Task.checkCancellation()
}
var value = UInt64(index)
for round in 0..<rounds {
if round % 4096 == 0 {
try Task.checkCancellation()
}
value = (value &* 1_664_525) &+ 1_013_904_223
}
total = total &+ value
}
try Task.checkCancellation()
return total
}
}
Each child captures immutable bounds and returns one integer. Only the parent task updates total; children never append to a shared collection or modify interface state. Task groups support concurrent child execution, require sendable results, and keep child lifetimes within the group’s scope. See Apple’s TaskGroup documentation.
Append this interface code to the same file:
@main
@MainActor
struct ParallelCalculationApp: App {
var body: some Scene {
WindowGroup {
CalculationView()
}
}
}
@MainActor
struct CalculationView: View {
@State private var status = "Ready"
@State private var work: Task<Void, Never>?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Synthetic Calculation")
.font(.title2)
Text(status)
.textSelection(.enabled)
HStack {
Button("Calculate") {
startCalculation()
}
.disabled(work != nil)
Button("Cancel") {
work?.cancel()
}
.disabled(work == nil)
}
}
.padding(24)
.frame(minWidth: 440, minHeight: 180)
.onDisappear {
work?.cancel()
}
}
private func startCalculation() {
guard work == nil else { return }
status = "Calculating…"
let workerLimit = max(
1,
ProcessInfo.processInfo.activeProcessorCount
)
work = Task { @MainActor in
defer { work = nil }
do {
let checksum = try await SyntheticCalculator.run(
count: 1_000_000,
rounds: 128,
workerLimit: workerLimit
)
try Task.checkCancellation()
status = "Checksum: \(checksum)"
} catch is CancellationError {
status = "Cancelled"
} catch {
status = "Calculation failed: \(error)"
}
}
}
}
The interface owns the outer task handle so the Cancel button can cancel it. Its task remains main-actor isolated, calls the concurrent calculation, and updates the displayed status after awaiting the result. Holding the handle also permits cancellation when the view disappears. Apple’s Task documentation explains that discarding a handle does not cancel the task.
The worker limit starts from Foundation’s activeProcessorCount, which reports active processing cores available on the computer. Treat this as an initial sizing choice to measure, rather than a guaranteed optimal task count. Its documented meaning is in activeProcessorCount.
Check correctness before comparing performance
Begin with the small illustrative input: change the call to count: 17, rounds: 4, and workerLimit: 3. The expected checksum is 18149427060254280572. Repeat with workerLimit: 1; the value must match. Also check count: 0, which must return zero, and a positive count smaller than the worker limit, which must still include every index.
The illustrative checksum and range coverage were checked with a small Python reproduction of the wrapping arithmetic. A Swift compiler was unavailable in the research environment, so the Swift program has not been compiled or executed here.
Next restore the larger input and compare optimized builds using worker limits of one, two, and the active processor count. Keep the count and rounds unchanged, retain the resulting checksum, and measure several runs. This is a proposed measurement procedure; no timing or speedup is asserted.
A one-worker run uses the same concurrent boundary and task-group structure, making it a useful comparison for this implementation. Small workloads may not justify partitioning and scheduling overhead. If a realistic calculation gives different indices substantially different costs, equal-size ranges may also leave one worker finishing later; smaller chunks with a bounded number of in-flight tasks are a reasonable next experiment.
For an array-producing calculation, attach a chunk index to each result and assemble chunks in input order. Task-group children may execute in any order, and the group exposes completed results as they become available. The checksum avoids ordering concerns only because its chosen reduction is order-independent. See TaskGroup.
Make cancellation and isolation failures visible
Cancellation is cooperative. Calling cancel() marks the task; calculation code must observe that state. The checks in both loops give the worker opportunities to throw, including when a single index has many rounds. Those intervals are implementation choices, not a promised cancellation latency. Apple documents this responsibility in Task.
The throwing result loop also matters. If a child throws and its error propagates out of the group body, the remaining children are cancelled. The group still waits for them to finish, so their cancellation checks remain necessary. The Swift standard-library implementation documents this behavior in TaskGroup.swift.
If the interface still stalls, trace the full expensive call path. Look for a worker calling a main-actor method, accessing actor-isolated properties, or performing substantial preparation before reaching the concurrent boundary. Pass the required integer values into the worker and keep the lengthy loops inside that boundary.
If the compiler rejects @concurrent, check the toolchain version. The attribute was implemented in Swift 6.2 and applies to asynchronous functions. For an older toolchain, the earlier nonisolated asynchronous execution model is documented in SE-0461; use documentation matching that compiler rather than assuming all versions give nonisolated async identical behavior.
For this Mac task, the useful architecture is small: the main actor owns interface state, concurrent workers receive independent values, and the parent combines complete results. Preserve the value contract as you tune chunk sizes, and verify both checksum equality and interface responsiveness before keeping a performance change.
Sources
- Embracing Swift concurrency — Apple.
- Run nonisolated async functions on the caller’s actor by default — Swift.org, Swift Evolution.
- Allow nonisolated to prevent global actor inference — Swift.org, Swift Evolution.
- Concurrency — Swift.org, The Swift Programming Language.
- Advanced Operators — Swift.org, The Swift Programming Language.
- Task — Apple Developer Documentation.
- TaskGroup — Apple Developer Documentation.
- activeProcessorCount — Apple Developer Documentation.
- TaskGroup.swift — Swift.org, Swift standard library.