concurrency-patterns

Concurrency Patterns — Expert Decisions

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "concurrency-patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-concurrency-patterns

Concurrency Patterns — Expert Decisions

Expert decision frameworks for Swift concurrency choices. Claude knows async/await syntax — this skill provides judgment calls for pattern selection and isolation boundaries.

Decision Trees

async let vs TaskGroup

Is the number of concurrent operations known at compile time? ├─ YES (2-5 fixed operations) │ └─ async let │ async let user = fetchUser() │ async let posts = fetchPosts() │ let (user, posts) = await (try user, try posts) │ └─ NO (dynamic count, array of IDs) └─ TaskGroup try await withThrowingTaskGroup(of: User.self) { group in for id in userIds { group.addTask { ... } } }

async let gotcha: All async let values MUST be awaited before scope ends. Forgetting to await silently cancels the task — no error, just missing data.

Task vs Task.detached

Does the new task need to inherit context? ├─ YES (inherit priority, actor, task-locals) │ └─ Task { } │ Example: Continue work on same actor │ └─ NO (fully independent execution) └─ Task.detached { } Example: Background processing that shouldn't block UI

The trap: Task { } inside @MainActor runs on MainActor. For truly background work, use Task.detached(priority:) .

Actor vs Class with Lock

Is the mutable state accessed from async contexts? ├─ YES → Actor (compiler-enforced isolation) │ └─ NO → Is it performance-critical? ├─ YES → Class with lock (less overhead) │ └─ Consider @unchecked Sendable if crossing boundaries │ └─ NO → Actor (safer, cleaner)

When actors lose: High-contention scenarios where lock granularity matters. Actor methods are fully isolated — can't lock just part of the state.

Sendable Conformance

Is the type crossing concurrency boundaries? ├─ NO → Don't add Sendable │ └─ YES → What kind of type? ├─ Struct with only Sendable properties │ └─ Implicit Sendable (or add explicit) │ ├─ Class with immutable state │ └─ Add Sendable, make let-only │ ├─ Class with mutable state │ └─ Is it manually thread-safe? │ ├─ YES → @unchecked Sendable │ └─ NO → Convert to actor │ └─ Closure └─ Mark @Sendable, capture only Sendable values

NEVER Do

Task & Structured Concurrency

NEVER create unstructured tasks for parallel work that should be grouped:

// ❌ No way to wait for completion, handle errors, or cancel func loadData() async { Task { try? await fetchUsers() } Task { try? await fetchPosts() } // Returns immediately, tasks orphaned }

// ✅ Structured — errors propagate, cancellation works func loadData() async throws { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { try await fetchUsers() } group.addTask { try await fetchPosts() } } }

NEVER assume Task.cancel() stops execution immediately:

// ❌ Assumes cancellation is synchronous task.cancel() let result = task.value // Task may still be running!

// ✅ Cancellation is cooperative — code must check func longOperation() async throws { for item in items { try Task.checkCancellation() // Or check Task.isCancelled await process(item) } }

NEVER forget that async let bindings auto-cancel if not awaited:

// ❌ profileImage is SILENTLY CANCELLED func loadUser() async throws -> User { async let user = fetchUser() async let profileImage = fetchImage() // Never awaited! return try await user // profileImage cancelled, no error }

// ✅ Await all async let bindings func loadUser() async throws -> (User, UIImage?) { async let user = fetchUser() async let profileImage = fetchImage() return try await (user, profileImage) // Both awaited }

Actor Isolation

NEVER ignore actor reentrancy:

// ❌ State can change during suspension actor BankAccount { var balance: Double = 100

func transferAll() async throws {
    let amount = balance  // Capture balance
    try await sendMoney(amount)  // Suspension point!
    balance = 0  // Balance might have changed since capture!
}

}

// ✅ Check state AFTER suspension actor BankAccount { var balance: Double = 100

func transferAll() async throws {
    let amount = balance
    try await sendMoney(amount)
    // Re-check or use atomic operation
    guard balance >= amount else {
        throw BankError.balanceChanged
    }
    balance -= amount
}

}

NEVER expose actor state as reference types:

// ❌ Array reference escapes actor isolation actor Cache { var items: [Item] = []

func getItems() -> [Item] {
    items  // Returns reference that can be mutated outside!
}

}

// ✅ Return copy or use value types actor Cache { private var items: [Item] = []

func getItems() -> [Item] {
    Array(items)  // Explicit copy
}

}

NEVER use nonisolated to bypass safety without understanding implications:

// ❌ Dangerous — defeats actor protection actor DataManager { var cache: [String: Data] = [:]

nonisolated func unsafeAccess() -> [String: Data] {
    cache  // DATA RACE — accessing actor state without isolation!
}

}

// ✅ nonisolated only for immutable or independent state actor DataManager { let id = UUID() // Immutable — safe

nonisolated var identifier: String {
    id.uuidString  // Safe — accessing immutable state
}

}

@MainActor

NEVER access @Published from background without MainActor:

// ❌ Undefined behavior — may crash, may corrupt Task.detached { viewModel.isLoading = false // Background thread! }

// ✅ Explicit MainActor Task { @MainActor in viewModel.isLoading = false } // Or mark entire ViewModel as @MainActor

NEVER block MainActor with synchronous work:

// ❌ UI freezes during heavy computation @MainActor func processData() { let result = heavyComputation(data) // Blocks UI! display(result) }

// ✅ Offload to detached task @MainActor func processData() async { let result = await Task.detached { heavyComputation(data) }.value display(result) // Back on MainActor }

Continuations

NEVER resume continuation more than once:

// ❌ CRASHES — continuation resumed twice func fetchAsync() async throws -> Data { try await withCheckedThrowingContinuation { continuation in fetch { result in continuation.resume(returning: result) } fetch { result in // Oops, called again! continuation.resume(returning: result) // CRASH! } } }

// ✅ Ensure exactly-once resumption func fetchAsync() async throws -> Data { try await withCheckedThrowingContinuation { continuation in var hasResumed = false fetch { result in guard !hasResumed else { return } hasResumed = true continuation.resume(returning: result) } } }

NEVER forget to resume continuation:

// ❌ Task hangs forever if error path doesn't resume func fetchAsync() async throws -> Data { try await withCheckedThrowingContinuation { continuation in fetch { data, error in if let data = data { continuation.resume(returning: data) } // Missing else! Continuation never resumed if error } } }

// ✅ Handle all paths func fetchAsync() async throws -> Data { try await withCheckedThrowingContinuation { continuation in fetch { data, error in if let error = error { continuation.resume(throwing: error) } else if let data = data { continuation.resume(returning: data) } else { continuation.resume(throwing: FetchError.noData) } } } }

Essential Patterns

Task-Local Values

enum RequestContext { @TaskLocal static var requestId: String? @TaskLocal static var userId: String? }

// Set context for entire task tree func handleRequest() async { await RequestContext.$requestId.withValue(UUID().uuidString) { await RequestContext.$userId.withValue(currentUserId) { await processRequest() // All child tasks inherit context } } }

// Access anywhere in task tree func logEvent(_ message: String) { let requestId = RequestContext.requestId ?? "unknown" logger.info("[(requestId)] (message)") }

Cancellation-Aware Loops

func processItems(_ items: [Item]) async throws { for item in items { // Check at start of each iteration try Task.checkCancellation()

    // Or handle gracefully without throwing
    guard !Task.isCancelled else {
        await saveProgress(items: processedItems)
        return
    }

    await process(item)
}

}

AsyncStream from Delegate

func locationUpdates() -> AsyncStream<CLLocation> { AsyncStream { continuation in let delegate = LocationDelegate { location in continuation.yield(location) }

    continuation.onTermination = { @Sendable _ in
        delegate.stop()
    }

    delegate.start()
}

}

Quick Reference

Concurrency Pattern Selection

Pattern Use When Gotcha

async let

2-5 known parallel operations Must await all bindings

TaskGroup

Dynamic number of operations Results arrive out of order

Task { }

Fire-and-forget with context Inherits actor isolation

Task.detached

True background work No context inheritance

actor

Shared mutable state Reentrancy on suspension

Sendable Quick Check

Type Sendable?

Value types with Sendable properties ✅ Implicit

let -only classes ✅ Add conformance

Mutable classes with internal locking ⚠️ @unchecked Sendable

Mutable classes without locking ❌ Use actor instead

Closures ✅ If marked @Sendable

Red Flags

Smell Problem Fix

Task { } everywhere Losing structured concurrency Use TaskGroup

@unchecked Sendable on mutable class Potential data race Use actor or add locking

nonisolated accessing mutable state Data race Remove nonisolated

Continuation without all-paths handling Potential hang Handle every code path

Task.detached for everything Losing priority/cancellation Use structured Task { }

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review