Back to list
Kaakati

concurrency-patterns

by Kaakati

Enterprise-grade Rails development workflow with multi-agent orchestration, beads task tracking, and quality gates. Generic and portable across any Rails project.

2🍴 0📅 Jan 24, 2026

SKILL.md


name: concurrency-patterns description: "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" version: "3.0.0"

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

PatternUse WhenGotcha
async let2-5 known parallel operationsMust await all bindings
TaskGroupDynamic number of operationsResults arrive out of order
Task { }Fire-and-forget with contextInherits actor isolation
Task.detachedTrue background workNo context inheritance
actorShared mutable stateReentrancy on suspension

Sendable Quick Check

TypeSendable?
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

SmellProblemFix
Task { } everywhereLosing structured concurrencyUse TaskGroup
@unchecked Sendable on mutable classPotential data raceUse actor or add locking
nonisolated accessing mutable stateData raceRemove nonisolated
Continuation without all-paths handlingPotential hangHandle every code path
Task.detached for everythingLosing priority/cancellationUse structured Task { }

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

+10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon