Store the calculation’s task inside an actor before the first suspension, then let concurrent callers await that same task. This walkthrough builds a local sum-of-squares example for a Mac utility, with explicit rules for cancellation, errors, and cleanup.

What should concurrent callers share?

Suppose two parts of a Mac desktop utility need the same calculated summary: a preview and a details panel. Each asks for the sum of the squares in the synthetic array [1, 2, 3, 4]. Both should receive 30, and requests that join the same active run should share one execution.

The implementation below gives each calculator instance an immutable input array. It shares the current task while that task occupies the actor’s flight slot, then clears the slot when a caller observes completion. A subsequent request can start another calculation. This is sharing work in progress; retaining completed answers indefinitely would require a separate caching policy.

The value contract is deliberately small:

  • Accept at most 100,000 integers, each between 0 and 1,000.
  • Return the sum of their squares; an empty array returns 0.
  • Attach a run identifier so callers can distinguish shared execution from merely equal totals.
  • Propagate calculation errors to callers awaiting that run.
  • Let an individual caller cancel its interest without canceling the shared worker.

The maximum permitted total is 100,000 × 1,000² = 100,000,000,000, which fits the Mac’s 64-bit Int. These limits belong to this example’s contract. Changing them requires reconsidering arithmetic overflow and resource use.

Why does an actor still need a stored task?

An actor protects its mutable state, but an asynchronous actor method can suspend. During that suspension, another call can enter the actor. Swift’s language guide explains actor isolation and suspension points, while Apple’s actor session demonstrates how assumptions made before an await can become outdated. Concurrency — The Swift Programming Language, Protect mutable state with Swift actors.

Consequently, checking for an existing answer and then awaiting a fresh calculation does not prevent duplicate work. Another caller can arrive before the answer exists. Instead, publish the task handle synchronously: inspect the flight slot, create a worker only if the slot is empty, store it, and then await its result.

Apple documents that a task may begin immediately after creation. Accessing its asynchronous value waits for completion and propagates a thrown error. The actor therefore coordinates ownership of one handle; callers obtain their results through that handle. Task, value.

Build the complete local example

Use a Swift 6 compiler on macOS 13 or later. The macOS requirement here comes from the demonstration’s Task.sleep(for:) call. That call suspends without blocking its underlying thread; the short pause makes overlapping requests easier to observe. It performs no part of the arithmetic and can be removed afterward. sleep(for:tolerance:clock:).

Save the following complete program as SharedCalculation.swift. It uses ordinary nonisolated default settings for the command-line compilation shown below. Foundation supplies UUID, whose documented conformances include Sendable and Equatable; the identifier labels each flight and supports conditional cleanup. UUID.

import Foundation

struct CalculationResult: Sendable {
    let total: Int
    let runID: UUID
}

enum CalculationError: Error {
    case invalidInput
}

actor SharedCalculation {
    private struct Flight {
        let id: UUID
        let task: Task<CalculationResult, Error>
    }

    private let samples: [Int]
    private var flight: Flight?

    init(samples: [Int]) {
        self.samples = samples
    }

    func value() async throws -> CalculationResult {
        try Task.checkCancellation()

        let active: Flight
        if let existing = flight {
            active = existing
        } else {
            let id = UUID()
            let snapshot = samples
            let task = Task.detached(priority: .userInitiated) {
                [snapshot, id] () async throws -> CalculationResult in
                guard snapshot.count <= 100_000,
                      snapshot.allSatisfy({ (0...1_000).contains($0) })
                else {
                    throw CalculationError.invalidInput
                }

                // Demonstration only: allow overlap to be observed.
                try await Task.sleep(for: .milliseconds(100))
                let total = snapshot.reduce(0) { sum, sample in
                    sum + sample * sample
                }
                return CalculationResult(total: total, runID: id)
            }
            active = Flight(id: id, task: task)
            flight = active
        }

        defer {
            // An older waiter must not clear a newer flight.
            if flight?.id == active.id {
                flight = nil
            }
        }

        let result = try await active.task.value
        try Task.checkCancellation()
        return result
    }
}

@main
struct Demo {
    static func main() async throws {
        let calculation = SharedCalculation(samples: [1, 2, 3, 4])

        async let preview = calculation.value()
        async let details = calculation.value()
        async let inspector = calculation.value()

        let (a, b, c) = try await (preview, details, inspector)
        precondition(a.total == 30 && b.total == 30 && c.total == 30)

        let observedRuns = Set([a.runID, b.runID, c.runID]).count
        print("Total: \(a.total)")
        print("Runs observed across three requests: \(observedRuns)")

        let later = try await calculation.value()
        precondition(later.total == 30)
        print("Later request started another run: \(later.runID != c.runID)")
    }
}

Compile and run it from the directory containing the file:

swiftc -swift-version 6 -parse-as-library SharedCalculation.swift -o SharedCalculation
./SharedCalculation

The arithmetic assertions should pass. When all three initial calls enter before the flight is cleared, the observed run count is 1. The later request arrives after those calls return, so it starts another calculation. The pause encourages overlap but does not guarantee scheduling: an initial request delayed long enough could legitimately start a later flight. Swift was unavailable in the research environment, so this program was inspected but not compiled or executed here.

The worker uses a detached task because this calculation belongs to the shared calculator rather than to one requesting caller, and its arithmetic should run independently of the coordinating actor. It captures only the input snapshot and identifier. Apple recommends structured child tasks when they can express the required lifetime; detached tasks require explicit lifetime and cancellation decisions. The example also supplies its priority explicitly. detached(name:priority:operation:).

How does cleanup avoid clearing the next calculation?

The check-and-store sequence contains no await, so a second actor call cannot slip between checking the empty slot and publishing the new flight. Once the first caller suspends on active.task.value, another caller can enter, find that flight, and await the same handle.

Cleanup needs its own identity check because several callers can resume from the same completed task at different times. Consider this sequence:

Event Flight slot
Callers A and B join run X X
X finishes; A resumes and clears X Empty
Caller C enters and starts run Y Y
B resumes from X and performs cleanup Y must remain

An unconditional assignment of nil by B would erase Y’s handle. A fourth caller could then start duplicate work while Y was still running. Comparing flight?.id with active.id makes cleanup specific to the run each caller actually joined. This sequence is a consequence of the implementation and the documented reentrancy behavior. Protect mutable state with Swift actors.

The exact sharing boundary is the stored flight slot. A worker may already have finished while its waiting callers have not yet resumed to clear the slot. A request entering during that interval can still receive that completed task’s result. After the slot is cleared, a new request starts a new run. This brief completion interval is part of the contract.

What happens when a caller cancels or the calculation fails?

Cancellation is cooperative in Swift: code must decide where to check for it and what to do. This example checks the requesting task before joining and again after successfully receiving the shared result. Task — Task Cancellation.

A caller already canceled at the first check throws without starting or joining work. A caller canceled while waiting continues to await the shared worker; after a successful worker result, its second check throws CancellationError. Other callers can still receive the result because this implementation never forwards an individual caller’s cancellation to the worker.

This policy does not provide immediate cancellation of the wait. If every caller cancels, the worker still finishes. That is an intentional choice for this bounded local calculation. A design requiring prompt individual departure would need additional waiter bookkeeping and a clearly defined rule for when the shared worker should stop.

Invalid input causes the worker to throw CalculationError.invalidInput. Callers already sharing that task receive its error through value, and their deferred cleanup releases the matching slot. A later request retries the same immutable input and fails again. Nothing silently substitutes a total or drops invalid elements. value.

There is also an explicit error-ordering detail: if a caller is canceled while the worker fails, the worker’s error propagates before the second cancellation check. If the worker succeeds, the caller’s cancellation check determines whether that caller returns the result.

How should you verify and use the pattern in a Mac utility?

Equal totals alone do not demonstrate shared execution: three separate calculations of this input also produce 30. Compare run identifiers when checking overlap, and verify the numerical result separately.

For a deterministic concurrency test, replace the demonstration pause with a controllable asynchronous gate and instrument admission to the flight. Hold the worker until all intended callers have joined, then release it. Check one worker start, matching identifiers, correct totals, error propagation, and a fresh run after cleanup. Also exercise the cleanup sequence in the table. Increasing the sleep duration cannot establish those conditions reliably.

In a desktop app, keep one calculator instance for consumers that use the same input. Creating a new instance for every panel gives each panel an independent flight slot. When the synthetic input changes, create a new calculator for that snapshot; this example does not define mutable inputs or keyed sharing.

Apply returned values to UI state on the main actor. Swift’s language guide documents @MainActor for UI work; the shared calculator can continue to coordinate its own state separately. Concurrency — The Swift Programming Language.

For a tiny sum, task creation is unnecessary overhead; the sample makes the ownership rules visible so they can be applied to a more substantial local calculation. Preserve the essential sequence when adapting it: publish one task before suspension, await that handle, and clear only the flight that completed.

Sources