concurrency-patterns
npx skills add https://github.com/kaakati/rails-enterprise-dev --skill concurrency-patterns
Agent 安装分布
Skill 文档
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 { } |