ax-concurrency
npx machina-cli add skill Kasempiternal/axiom-v2/ax-concurrency --openclawConcurrency
Quick Patterns
Progressive Journey (Start Simple)
Single-Threaded → Asynchronous → Concurrent → Actors
| | | |
Start here Hide latency Background Move data
(network) CPU work off main
Advance only when profiling proves it's needed.
- Stay single-threaded if UI is responsive and operations are fast (<16ms)
- Add async/await when high-latency operations (network, file I/O) block UI
- Add @concurrent when CPU-intensive work freezes UI (image processing, parsing)
- Add actors when too much main actor code causes contention
Main Actor Mode (Xcode 26+)
Build Settings → Swift Compiler — Language
→ "Default Actor Isolation" = Main Actor
→ "Approachable Concurrency" = Yes
Build Settings → Swift Compiler — Concurrency
→ "Strict Concurrency Checking" = Complete
All code protected by @MainActor by default unless explicitly marked otherwise.
Async/Await (Hide Latency)
// Suspends without blocking main thread
func fetchAndDisplay(url: URL) async throws {
let (data, _) = try await URLSession.shared.data(from: url) // Suspends here
let image = decodeImage(data) // Resumes on main thread
view.displayImage(image)
}
// Create task for user action
func onTapEvent() {
Task {
do {
try await fetchAndDisplay(url: url)
} catch {
displayError(error)
}
}
}
@concurrent (Background CPU Work, Swift 6.2+)
// Always runs on background thread pool
@concurrent
func decodeImage(_ data: Data) async -> UIImage {
// Image processing, parsing, heavy computation
return UIImage(data: data)!
}
// Usage - automatically offloads
let image = await decodeImage(data)
Use @concurrent when work should always run on background (image processing, parsing).
Use nonisolated when caller decides where work runs (library APIs).
@MainActor for UI Code
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
@Published var isPlaying: Bool = false
func play(_ track: Track) async {
self.currentTrack = track
self.isPlaying = true
// Already on MainActor - direct state access
}
}
Actors (Move Data Off Main Thread)
// Extract non-UI subsystems into actors
actor NetworkManager {
var openConnections: [URL: Connection] = [:]
func openConnection(for url: URL) -> Connection {
if let existing = openConnections[url] { return existing }
let connection = Connection()
openConnections[url] = connection
return connection
}
}
// Access requires await
let connection = await networkManager.openConnection(for: url)
Use actors for: Non-UI subsystems with independent state (cache, database, network).
Do NOT use actors for: ViewModels, View Controllers, model classes (use @MainActor).
Sendable Types
// Value types are Sendable (copy when passed between actors)
struct Track: Sendable {
let id: UUID
let title: String
let duration: TimeInterval
}
enum PlaybackState: Sendable {
case stopped, playing, paused
}
// Classes are NOT Sendable by default
// Keep @MainActor or non-Sendable (don't share concurrently)
@MainActor class ViewModel: ObservableObject { } // Implicitly Sendable
actor Cache { } // Implicitly Sendable
Delegate Value Capture (Critical Pattern)
// nonisolated delegate method needs to update @MainActor state
nonisolated func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
// Step 1: Capture values BEFORE Task
let location = locations.last
// Step 2: Hop to MainActor
Task { @MainActor in
// Step 3: Safe to access self
self.currentLocation = location
}
}
Weak Self in Stored Tasks
class Player {
private var progressTask: Task<Void, Never>?
func startMonitoring() {
progressTask = Task { [weak self] in
guard let self else { return }
while !Task.isCancelled {
await self.updateProgress()
}
}
}
deinit { progressTask?.cancel() }
}
assumeIsolated (Synchronous Actor Access)
// When you KNOW you're on the correct actor (documented guarantee)
// CLLocationManager delivers callbacks on main thread when created there
@MainActor class LocationDelegate: NSObject, CLLocationManagerDelegate {
var location: CLLocation?
nonisolated func locationManager(
_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]
) {
MainActor.assumeIsolated {
self.location = locations.last // Synchronous, no Task overhead
}
}
}
// @preconcurrency equivalent (cleaner syntax)
extension MyClass: @preconcurrency SomeDelegate {
func callback() {
self.updateUI() // Compiler wraps in assumeIsolated
}
}
// DANGER: Crashes if not actually on the correct actor!
// Only use when callback isolation is documented/guaranteed
Task vs assumeIsolated
| Aspect | Task { @MainActor in } | MainActor.assumeIsolated |
|---|---|---|
| Timing | Deferred (next run loop) | Synchronous (inline) |
| Async support | Yes (can await) | No (sync only) |
| Failure mode | Runs anyway | Crashes if wrong isolation |
| Use case | Start async work | Verified synchronous access |
Mutex (Thread-Safe Primitives)
import Synchronization
// Mutex (iOS 18+) - faster than actor for microsecond operations
let counter = Mutex<Int>(0)
counter.withLock { $0 += 1 }
let value = counter.withLock { $0 }
// Thread-safe wrapper
final class ThreadSafeValue<T: Sendable>: @unchecked Sendable {
private let mutex: Mutex<T>
init(_ value: T) { mutex = Mutex(value) }
var value: T {
get { mutex.withLock { $0 } }
set { mutex.withLock { $0 = newValue } }
}
}
// Fast sync read in actor (bypass actor hop)
actor ImageCache {
private let mutex = Mutex<[URL: Data]>([:])
nonisolated func cachedSync(_ url: URL) -> Data? {
mutex.withLock { $0[url] }
}
func cacheAsync(_ url: URL, data: Data) {
mutex.withLock { $0[url] = data }
}
}
OSAllocatedUnfairLock (iOS 16+)
import os
let lock = OSAllocatedUnfairLock(initialState: 0)
lock.withLock { state in state += 1 }
// Fallback for iOS 16-17
#if compiler(>=6.0)
import Synchronization
typealias Lock<T> = Mutex<T>
#else
import os
// Use OSAllocatedUnfairLock
#endif
Atomic Types (iOS 18+)
import Synchronization
let counter = Atomic<Int>(0)
counter.wrappingAdd(1, ordering: .relaxed)
let (exchanged, original) = counter.compareExchange(
expected: 0, desired: 42,
ordering: .acquiringAndReleasing
)
Ownership Modifiers (Performance)
// borrowing: Read-only, no copy (for large value types)
func process(_ buffer: borrowing LargeBuffer) -> Int {
buffer.data.count // No copy of large array
}
// consuming: Transfer ownership (for factory/builder patterns)
struct Builder {
consuming func build() -> Product {
Product(config: config) // Builder invalid after call
}
}
// ~Copyable: Enforce single ownership
struct FileHandle: ~Copyable {
borrowing func read(count: Int) -> Data { ... }
consuming func close() { Darwin.close(fd) }
deinit { Darwin.close(fd) }
}
When to use ownership: Large structs in tight loops, ARC traffic visible in profiler, ~Copyable types. Skip for: Small types, most code (compiler optimizes well).
Decision Tree
Concurrency task?
├─ Starting new feature
│ ├─ UI responsive with sync code? → Stay single-threaded
│ ├─ High-latency operation? → async/await
│ ├─ CPU-intensive blocking UI? → @concurrent (Swift 6.2+) or nonisolated
│ └─ Main actor contention? → Extract to actor
│
├─ Error: "actor-isolated property accessed from nonisolated context"
│ ├─ In delegate method? → Capture values, Task { @MainActor in }
│ ├─ In async function? → Add @MainActor or use Task
│ └─ In @concurrent function? → Move access to caller or use await
│
├─ Error: "Type does not conform to Sendable"
│ ├─ Struct/enum with Sendable properties? → Add : Sendable
│ └─ Class? → Make @MainActor, actor, or keep non-Sendable
│
├─ Need synchronization
│ ├─ Lock-free counter/flag? → Atomic (iOS 18+)
│ ├─ Microsecond operations? → Mutex (iOS 18+) or OSAllocatedUnfairLock
│ ├─ Need suspension points (await)? → Actor
│ ├─ Cross-await access? → Actor
│ └─ Performance-critical hot path? → Mutex/Atomic
│
├─ Delegate callback needs actor access
│ ├─ Documented main thread delivery? → assumeIsolated
│ ├─ Unknown delivery context? → Task { @MainActor in }
│ └─ Protocol will add isolation? → @preconcurrency conformance
│
└─ Performance optimization
├─ Large value type copies? → borrowing parameter
├─ ARC traffic in profiler? → borrowing for reference types
├─ Single-use builder? → consuming method
└─ Unique resource handle? → ~Copyable
Anti-Patterns
// ---- Premature concurrency ----
// WRONG: Concurrency for trivial work
@concurrent
func add(_ a: Int, _ b: Int) async -> Int { a + b }
// FIX: Stay synchronous
func add(_ a: Int, _ b: Int) -> Int { a + b }
// ---- Making ViewModel an actor ----
// WRONG: UI code should be @MainActor
actor MyViewModel: ObservableObject {
@Published var state: State // Won't work correctly
}
// FIX: @MainActor for UI-facing classes
@MainActor class MyViewModel: ObservableObject {
@Published var state: State
}
// ---- Strong self in stored tasks ----
// WRONG: Memory leak
progressTask = Task {
while true { await self.update() }
}
// FIX: Weak capture
progressTask = Task { [weak self] in
guard let self else { return }
while !Task.isCancelled { await self.updateProgress() }
}
// ---- Holding lock across await ----
// WRONG: Deadlock risk
mutex.withLock {
await someAsyncWork() // Task suspends while holding lock!
}
// FIX: Release before await
let value = mutex.withLock { $0 }
let result = await process(value)
mutex.withLock { $0 = result }
// ---- DispatchSemaphore in async context ----
// WRONG: Blocks cooperative thread → thread pool exhaustion
Task {
semaphore.wait() // NEVER in async context
}
// FIX: Use async continuation
await withCheckedContinuation { continuation in
callback { continuation.resume() }
}
// ---- os_unfair_lock in Swift ----
// WRONG: Lock may move in memory
var lock = os_unfair_lock()
os_unfair_lock_lock(&lock) // Undefined behavior
// FIX: Use OSAllocatedUnfairLock (heap-allocated, stable)
let lock = OSAllocatedUnfairLock()
// ---- assumeIsolated to silence warnings ----
// WRONG: Crashes if not actually on correct actor
func unknownContext() {
MainActor.assumeIsolated { updateUI() } // May crash!
}
// FIX: Use async when uncertain
func unknownContext() async {
await MainActor.run { updateUI() }
}
// ---- Recursive locking ----
// WRONG: OSAllocatedUnfairLock is non-recursive
lock.withLock {
doWork() // If doWork() also locks → deadlock
}
// FIX: Extract data, operate outside lock
let data = lock.withLock { $0.copy() }
doWork(with: data)
// ---- Over-optimizing with ownership ----
// WRONG: Unnecessary for small types
func add(_ a: borrowing Int, _ b: borrowing Int) -> Int { a + b }
// FIX: Let compiler optimize
func add(_ a: Int, _ b: Int) -> Int { a + b }
Deep Patterns
Background SwiftData Access
actor DataFetcher {
let modelContainer: ModelContainer
func fetchAllTracks() throws -> [Track] {
let context = ModelContext(modelContainer)
return try context.fetch(FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
))
}
}
// Core Data thread-safe fetch
actor CoreDataFetcher {
func fetchTrackIDs(genre: String) async throws -> [String] {
let context = persistentContainer.newBackgroundContext()
var trackIDs: [String] = []
try await context.perform {
let request = NSFetchRequest<CDTrack>(entityName: "Track")
request.predicate = NSPredicate(format: "genre = %@", genre)
trackIDs = try context.fetch(request).map(\.id)
}
return trackIDs
}
}
Batch Import with Progress
actor DataImporter {
func importRecords(
_ records: [RawRecord],
onProgress: @MainActor (Int, Int) -> Void
) async throws {
let context = ModelContext(modelContainer)
let chunkSize = 1000
for (index, chunk) in records.chunked(into: chunkSize).enumerated() {
for record in chunk {
context.insert(Track(from: record))
}
try context.save()
let processed = min((index + 1) * chunkSize, records.count)
await onProgress(processed, records.count)
if Task.isCancelled { throw CancellationError() }
}
}
}
Isolated Protocol Conformances (Swift 6.2+)
protocol Exportable {
func export()
}
// Conform with explicit isolation
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG() // Safe: both on MainActor
}
}
Breaking Main Actor Ties for @concurrent
@MainActor class ImageModel {
var cachedImage: [URL: UIImage] = [:]
func fetchAndDisplay(url: URL) async throws {
// Strategy 1: Check cache on main actor BEFORE background work
if let image = cachedImage[url] {
view.displayImage(image)
return
}
let (data, _) = try await URLSession.shared.data(from: url)
let image = await decodeImage(data)
view.displayImage(image)
}
@concurrent
func decodeImage(_ data: Data) async -> UIImage {
// No main actor access needed - clean background work
UIImage(data: data)!
}
}
Synchronization Decision Matrix
| Need | Mutex | Atomic | Actor | OSAllocatedUnfairLock |
|---|---|---|---|---|
| iOS 18+ | Yes | Yes | Yes | Yes |
| iOS 16+ | No | No | Yes | Yes |
| Sendable | Yes | Yes | Yes | Yes |
| Async support | No | No | Yes | No |
| Lock-free | No | Yes | No | No |
| Microsecond ops | Yes | Yes | No | Yes |
| Complex state | Yes | No | Yes | Yes |
Memory Ordering for Atomics
| Ordering | Use Case |
|---|---|
.relaxed | Counters, no dependencies between operations |
.acquiring | Load before dependent operations |
.releasing | Store after dependent operations |
.acquiringAndReleasing | Read-modify-write |
.sequentiallyConsistent | Strongest guarantee (rarely needed) |
~Copyable Limitations
| Limitation | Impact |
|---|---|
| Can't store in Array, Dictionary, Set | Collections require Copyable |
| Can't use with most generics | <T> implicitly means <T: Copyable> |
| Protocol conformance restricted | Most protocols require Copyable |
| Can't capture in closures by default | Closures copy captured values |
| No existential support | any ~Copyable doesn't work |
Prefer consuming func on regular types as a lighter alternative for "use once" semantics.
Diagnostics
Concurrency issue?
├─ "Sending 'self' risks causing data races"
│ ├─ In delegate? → Capture values before Task
│ ├─ Passing object to another actor? → Make Sendable or copy data
│ └─ Class crossing boundaries? → Keep @MainActor or make struct
│
├─ "Main actor-isolated property accessed from nonisolated context"
│ ├─ Delegate callback → Pattern: capture + Task { @MainActor in }
│ ├─ @concurrent function → Move access to caller or use await
│ └─ Protocol conformance → @preconcurrency or isolated conformance
│
├─ UI freezing (main thread blocked)
│ ├─ Check Instruments → Swift Concurrency template
│ ├─ Long blue bars on main? → Offload with @concurrent
│ ├─ Sync code in async function? → Add await/suspension
│ └─ Heavy work on MainActor? → Extract to actor
│
├─ Actor contention (serialized parallel work)
│ ├─ High red:blue ratio in Instruments → Too much serialization
│ ├─ Heavy work inside actor? → Mark nonisolated
│ ├─ Single actor bottleneck? → Split into multiple actors
│ └─ Hot path through actor? → Use Mutex for fast reads
│
├─ Thread pool exhaustion
│ ├─ Tasks queued but not executing → Blocking call detected
│ ├─ DispatchSemaphore in Task? → Use async continuation
│ ├─ Sync file I/O in Task? → Use async APIs
│ └─ Debug: SWIFT_CONCURRENCY_COOPERATIVE_THREAD_BOUNDS=1
│
├─ assumeIsolated crash
│ ├─ Not actually on expected actor → Use async/await instead
│ ├─ GCD main queue != MainActor (edge case) → Check docs
│ └─ Only use when callback isolation is documented
│
└─ Deadlock
├─ Lock held across await? → Release before await
├─ Recursive lock attempt? → Non-recursive locks deadlock
├─ Nested withLock calls? → Extract data, operate outside
└─ Semaphore in cooperative pool? → Use continuation
Related
ax-concurrency-ref- Instruments workflows for profiling concurrencyax-swiftdata- Background ModelContext patternsax-core-data- Thread-safe NSManagedObjectContextax-grdb- Background database operations
Source
git clone https://github.com/Kasempiternal/axiom-v2/blob/main/axiom-plugin/skills/ax-concurrency/SKILL.mdView on GitHub Overview
ax-concurrency gathers practical Swift 6 concurrency patterns, from async/await and @MainActor to actors and Sendable types. It promotes progressive adoption to keep UI responsive and to safely isolate state across concurrency domains. The framework emphasizes gradual complexity, moving CPU work off the main thread and clearly separating UI from non-UI subsystems.
How This Skill Works
The skill leverages Swift's concurrency model: default @MainActor protection for UI code, explicit actors for isolated state, and @concurrent for CPU-bound work. Data is passed using Sendable types, with nonisolated and ownership modifiers guiding where work runs and how state is accessed, supported by examples like asynchronous fetch patterns and background decoding.
When to Use It
- When building UI-centred apps where the main thread must stay responsive (e.g., SwiftUI), and you want safe defaults via Main Actor.
- When performing high-latency operations (network requests, file I/O) that should not block the UI thread.
- When CPU-intensive tasks freeze the UI and you want background processing (e.g., image processing, parsing).
- When you need to move non-UI subsystems (cache, database, network) off the main thread using actors.
- When adopting a progressive concurrency strategy and ensuring concurrency checks are complete and visible (Main Actor, @concurrent, actors).
Quick Start
- Step 1: Identify latency versus CPU-bound work and plan a progressive adoption path (single-threaded → async/await → @concurrent → actors).
- Step 2: Annotate UI-related code with @MainActor and extract non-UI logic into actors for isolation.
- Step 3: Introduce async/await for network/file I/O, apply @concurrent for heavy CPU tasks, and define Sendable data models.
Best Practices
- Start with a single-threaded approach and progressively add async/await only after profiling shows a latency bottleneck.
- Default UI code to @MainActor to ensure thread-safe state access and predictable UI updates.
- Use @concurrent for CPU-bound work that should always run on a background thread pool.
- Extract non-UI subsystems into actors to move data and state off the main thread.
- Prefer Sendable value types for data sharing; mark references (classes) with @MainActor or non-Sendable constraints when needed.
Example Use Cases
- Progressive Journey: Single-Threaded → Asynchronous → Concurrent → Actors (with stepwise adoption).
- Async/Await example: fetchAndDisplay uses await to suspend without blocking the main thread and updates UI on completion.
- @concurrent example: decodeImage marked as @concurrent runs on a background thread pool and can be awaited.
- @MainActor UI pattern: PlayerViewModel demonstrates UI state updates inside a MainActor-protected class.
- Actors usage: NetworkManager shows how to move non-UI state into an actor and await access to shared resources.