Use one long-lived Foundation OperationQueue and set its maxConcurrentOperationCount before submitting calculations. For a Mac utility receiving one request per button press, this caps simultaneous work while allowing later requests to wait.
This tutorial builds the calculation engine for that desktop task and a standalone program to check its behavior. Every request contains synthetic integers, and every result stays in memory. The calculation runs synchronously inside its operation, so the operation’s lifetime covers the actual work.
Which concurrency tool fits individual incoming requests?
An OperationQueue accepts operations individually, and its block-based addOperation(_:) method wraps each submitted closure in an operation. Keep the queue in an object that lives across requests, then submit one closure for each incoming item. Apple’s addOperation(_:) documentation documents this submission method.
Set maxConcurrentOperationCount to a positive limit such as 2. Apple defines this property as the maximum number of that queue’s operations that can execute simultaneously. The limit belongs to that particular queue; creating a separate queue for every request would defeat a shared limit. Reducing it later also leaves already executing operations running. Apple’s concurrency-limit documentation explains these boundaries.
A Swift task group serves a different lifetime. It belongs inside a withTaskGroup scope, cannot be used from outside the task that created it, and waits for its child tasks before returning. Consequently, storing a task group as a property for unrelated future button presses is unsupported. For this synchronous calculation engine, a retained operation queue supplies the required lifetime and explicit concurrency setting. Swift’s standard-library task-group documentation describes these restrictions.
| Your work arrangement | Suitable approach |
|---|---|
| Independent synchronous calculations arrive individually | Retain one OperationQueue and submit one operation per calculation. |
| A parent asynchronous operation owns a finite collection of child tasks | Use a task group within that parent’s scope; explicitly control admissions if a numeric limit is required. |
| Work must update the desktop interface | Perform the calculation separately, then deliver its result to code isolated to MainActor. |
The final row concerns result delivery. Swift’s language guide identifies the main actor as the place for interface state and demonstrates explicitly main-actor-isolated closures. Keep expensive arithmetic outside that delivery step. The Swift Programming Language: Concurrency explains main-actor isolation.
Define the calculation and its completion contract
The sample accepts an integer identifier and an iteration count from 1 through 1_000_000. For an iteration count of n, it adds the squares of all integers from 1 through n. It returns the identifier alongside a UInt64 result. The identifier labels the output; it does not affect the arithmetic.
These are illustrative limits chosen for this program. At the maximum accepted input, the result is 333333833333500000, which fits in UInt64. Validation happens before submission, so an invalid request throws immediately and creates no queued operation. The input restriction also keeps the loop range valid.
Each accepted request produces one stored result when its operation finishes. Results accumulate in an array, so repeated identifiers do not overwrite earlier results. The demonstration uses distinct ascending identifiers and sorts by identifier for readable output; that display order makes no claim about execution order.
This contract assumes the queue remains available and its operations run to completion. The example does not expose cancellation. Adding cancellation later requires a separate outcome for unfinished requests, because Apple documents cancellation as a request to stop, with executing work responsible for responding. Apple’s OperationQueue documentation describes operation lifetime and cancellation.
Build a complete program with a shared limit
Use a Mac with a Swift 6.2 or newer compiler and the macOS SDK available through Xcode or its command-line tools. Save the following complete program as main.swift. The commands below explicitly select nonisolated default isolation, keeping the calculation helpers independent of an app target’s isolation settings. Swift documents this compiler setting in SE-0466: Control default actor isolation inference.
The result log uses NSLock to protect both its array and its activity counters. Its @unchecked Sendable conformance places responsibility for that synchronization on the implementation: every access to mutable state takes the same lock. Apple documents this responsibility in Sendable and documents critical-section protection in NSLock.
import Foundation
enum InputError: Error {
case invalidIterations
}
struct Calculation: Sendable {
let id: Int
let iterations: Int
}
func sumOfSquares(_ job: Calculation) -> UInt64 {
var sum: UInt64 = 0
for integer in 1...job.iterations {
let value = UInt64(integer)
sum += value * value
}
return sum
}
final class ExecutionLog: @unchecked Sendable {
private let lock = NSLock()
private var active = 0
private var peak = 0
private var rows: [(id: Int, value: UInt64)] = []
func begin() {
lock.lock()
defer { lock.unlock() }
active += 1
peak = max(peak, active)
}
func finish(id: Int, value: UInt64) {
lock.lock()
defer { lock.unlock() }
rows.append((id: id, value: value))
active -= 1
}
func snapshot() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
lock.lock()
defer { lock.unlock() }
return (rows, peak)
}
}
final class CalculationPool {
private let queue = OperationQueue()
private let log = ExecutionLog()
init(limit: Int) {
precondition(limit > 0)
queue.name = "Synthetic calculations"
queue.maxConcurrentOperationCount = limit
}
func submit(id: Int, iterations: Int) throws {
guard (1...1_000_000).contains(iterations) else {
throw InputError.invalidIterations
}
let job = Calculation(id: id, iterations: iterations)
queue.addOperation { [log] in
log.begin()
let value = sumOfSquares(job)
log.finish(id: job.id, value: value)
}
}
// Call only after the producer has stopped submitting work.
// This blocking method is for the command-line demonstration.
func finishForDemo() -> (
rows: [(id: Int, value: UInt64)], peak: Int
) {
queue.waitUntilAllOperationsAreFinished()
return log.snapshot()
}
}
@main
struct Demo {
static func main() throws {
let limit = 2
let pool = CalculationPool(limit: limit)
try pool.submit(id: 1, iterations: 100_000)
try pool.submit(id: 2, iterations: 200_000)
try pool.submit(id: 3, iterations: 300_000)
let report = pool.finishForDemo()
let rows = report.rows.sorted { $0.id < $1.id }
precondition(rows.map { $0.id } == [1, 2, 3])
precondition(rows.map { $0.value } == [
UInt64(333338333350000),
UInt64(2666686666700000),
UInt64(9000045000050000)
])
precondition(report.peak >= 1 && report.peak <= limit)
for row in rows {
print("Job \(row.id): \(row.value)")
}
print("Observed peak: \(report.peak)")
}
}
Compile and run it from the directory containing main.swift:
swiftc -swift-version 6 -default-isolation nonisolated \
-parse-as-library main.swift -o calculation-demo
./calculation-demo
The three result lines should be:
Job 1: 333338333350000
Job 2: 2666686666700000
Job 3: 9000045000050000
The last line reports an observed peak of either 1 or 2. A limit permits overlap; it does not require it. The expected sums were independently checked using integer arithmetic and the formula n * (n + 1) * (2 * n + 1) / 6. The Swift program was inspected but could not be compiled in the research environment because no Swift executable was installed.
Why does this limit cover the actual calculation?
Each operation starts its activity counter, performs the entire calculation synchronously, records its result, and then returns. No additional asynchronous calculation escapes from that closure. Thus, limiting executing operations also limits these calculations.
Keep that relationship when adapting the program. If an operation merely creates a separate Task and immediately returns, the queue sees the operation as finished while that task may continue calculating. The operation limit would then govern the short launch closures. This follows from the documented operation lifetime and the block submission API. Apple’s OperationQueue overview and addOperation(_:) define those pieces.
The log’s lock covers only small bookkeeping sections. Holding it during sumOfSquares(_:) would serialize access to the calculation and prevent the intended overlap. Each lock acquisition and release stays within one synchronous method, matching Apple’s requirement that an NSLock be unlocked on the thread that locked it. Apple’s NSLock documentation explains that requirement.
To check an adaptation, run the same three requests with limit set to 1, then restore 2. Both runs must retain all three exact results. The serial run should report a peak of 1; the second run must stay within 2. These checks exercise result preservation and the observed calculation limit without assuming a particular scheduling order.
Connect incoming desktop actions without blocking the interface
Retain one CalculationPool in the component that owns the desktop calculation feature. Each incoming action calls submit(id:iterations:) once. Avoid recreating the pool inside the action handler, because every new instance has an independent queue and limit.
The demonstration calls finishForDemo() only after its final submission so the command-line process can collect results before exiting. This method blocks its calling thread. In a desktop app, use per-result delivery instead of calling it from a button handler or the main actor. Apple explicitly documents the blocking behavior of waitUntilAllOperationsAreFinished().
For interface integration, extend the submission API with a result callback isolated to MainActor and marked @Sendable. After computing a result inside the operation, schedule a Task with an explicit @MainActor closure to invoke that callback. Capture the immutable result value and the callback. Keep only the display update inside that closure; leave the arithmetic in the operation. Swift documents both main-actor closure syntax and sendable closure captures.
That interface version needs a further completion definition: finishing the calculation and displaying its result are separate events. The command-line program waits for calculations and synchronous result storage. A separately scheduled interface callback may execute afterward.
What happens when submissions outpace calculations?
The concurrency setting bounds executing operations, while additional submitted work can remain queued. It also leaves the sample’s completed-result array free to grow. Consequently, this example provides a calculation concurrency limit without a total memory or backlog limit. Apple documents that the queue retains operations until they finish. The OperationQueue overview explains retention.
If the desktop feature can generate sustained bursts, choose an admission policy before extending it: accept only a fixed number of outstanding requests and report rejection, pause further submissions, or replace a pending synthetic preview with a newer request when that behavior is explicitly intended. Coordinate admission counts and completion updates through one synchronized owner. The running-work limit alone cannot implement those policies.
For individual synchronous requests, the essential design is small: retain one queue, configure its limit once, keep each calculation inside its operation, and synchronize shared results. Preserve those boundaries as you connect desktop actions, then add an explicit backlog policy only when the arrival pattern requires it.
Sources
- OperationQueue — Apple.
- maxConcurrentOperationCount — Apple.
- addOperation(_:) — Apple.
- waitUntilAllOperationsAreFinished() — Apple.
- NSLock — Apple.
- Sendable — Apple.
- Concurrency — The Swift Programming Language — Swift.org.
- TaskGroup.swift — Swift standard-library source and documentation — Swift.org.
- SE-0466: Control default actor isolation inference — Swift.org.