smithery/Kaakati

concurrency-patterns

Expert Swift concurrency decisions: async let vs TaskGroup selection, actor isolation boundaries, @MainActor placement strategies, Sendable conformance judgment calls, and structured vs unstructured task trade-offs.

Installation

$ npx skills add smithery/Kaakati --skill concurrency-patterns

Summary

  • Expert Swift concurrency decisions: async let vs TaskGroup selection, actor isolation boundaries, @MainActor placement strategies, Sendable conformance judgment calls, and structured vs unstructured task trade-offs.
  • Use when designing concurrent code, debugging data races, or choosing between concurrency patterns.
  • Trigger keywords: async, await, actor, Task, TaskGroup, @MainActor, Sendable, concurrency, data race, isolation, structured concurrency, continuation

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/Kaakati · top by installs.

npx skills add smithery/Kaakati

Browse all from smithery/Kaakati

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Version3.0.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,417 B
  • docs SUMMARY.md 493 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

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 { }