Files
gh-glittercowboy-taches-cc-…/skills/expertise/macos-apps/references/concurrency-patterns.md
2025-11-29 18:28:37 +08:00

539 lines
12 KiB
Markdown

# Concurrency Patterns
Modern Swift concurrency for responsive, safe macOS apps.
<async_await_basics>
<simple_async>
```swift
// Basic async function
func fetchData() async throws -> [Item] {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Item].self, from: data)
}
// Call from view
struct ContentView: View {
@State private var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task {
do {
items = try await fetchData()
} catch {
// Handle error
}
}
}
}
```
</simple_async>
<task_modifier>
```swift
struct ItemListView: View {
@State private var items: [Item] = []
let category: Category
var body: some View {
List(items) { item in
Text(item.name)
}
// .task runs when view appears, cancels when disappears
.task {
await loadItems()
}
// .task(id:) re-runs when id changes
.task(id: category) {
await loadItems(for: category)
}
}
func loadItems(for category: Category? = nil) async {
// Automatically cancelled if view disappears
items = await dataService.fetchItems(category: category)
}
}
```
</task_modifier>
</async_await_basics>
<actors>
<basic_actor>
```swift
// Actor for thread-safe state
actor DataCache {
private var cache: [String: Data] = [:]
func get(_ key: String) -> Data? {
cache[key]
}
func set(_ key: String, data: Data) {
cache[key] = data
}
func clear() {
cache.removeAll()
}
}
// Usage (must await)
let cache = DataCache()
await cache.set("key", data: data)
let cached = await cache.get("key")
```
</basic_actor>
<service_actor>
```swift
actor NetworkService {
private let session: URLSession
private var pendingRequests: [URL: Task<Data, Error>] = [:]
init(session: URLSession = .shared) {
self.session = session
}
func fetch(_ url: URL) async throws -> Data {
// Deduplicate concurrent requests for same URL
if let existing = pendingRequests[url] {
return try await existing.value
}
let task = Task {
let (data, _) = try await session.data(from: url)
return data
}
pendingRequests[url] = task
defer {
pendingRequests[url] = nil
}
return try await task.value
}
}
```
</service_actor>
<nonisolated>
```swift
actor ImageProcessor {
private var processedCount = 0
// Synchronous access for non-isolated properties
nonisolated let maxConcurrent = 4
// Computed property that doesn't need isolation
nonisolated var identifier: String {
"ImageProcessor-\(ObjectIdentifier(self))"
}
func process(_ image: NSImage) async -> NSImage {
processedCount += 1
// Process image...
return processedImage
}
func getCount() -> Int {
processedCount
}
}
```
</nonisolated>
</actors>
<main_actor>
<ui_updates>
```swift
// Mark entire class as @MainActor
@MainActor
@Observable
class AppState {
var items: [Item] = []
var isLoading = false
var error: AppError?
func loadItems() async {
isLoading = true
defer { isLoading = false }
do {
// This call might be on background, result delivered on main
items = try await dataService.fetchAll()
} catch {
self.error = .loadFailed(error)
}
}
}
// Or mark specific functions
class DataProcessor {
@MainActor
func updateUI(with result: ProcessResult) {
// Safe to update UI here
}
func processInBackground() async -> ProcessResult {
// Heavy work here
let result = await heavyComputation()
// Update UI on main actor
await updateUI(with: result)
return result
}
}
```
</ui_updates>
<main_actor_dispatch>
```swift
// From async context
await MainActor.run {
self.items = newItems
}
// Assume main actor (when you know you're on main)
MainActor.assumeIsolated {
self.tableView.reloadData()
}
// Task on main actor
Task { @MainActor in
self.progress = 0.5
}
```
</main_actor_dispatch>
</main_actor>
<structured_concurrency>
<task_groups>
```swift
// Parallel execution with results
func loadAllCategories() async throws -> [Category: [Item]] {
let categories = try await fetchCategories()
return try await withThrowingTaskGroup(of: (Category, [Item]).self) { group in
for category in categories {
group.addTask {
let items = try await self.fetchItems(for: category)
return (category, items)
}
}
var results: [Category: [Item]] = [:]
for try await (category, items) in group {
results[category] = items
}
return results
}
}
```
</task_groups>
<limited_concurrency>
```swift
// Process with limited parallelism
func processImages(_ urls: [URL], maxConcurrent: Int = 4) async throws -> [ProcessedImage] {
var results: [ProcessedImage] = []
try await withThrowingTaskGroup(of: ProcessedImage.self) { group in
var iterator = urls.makeIterator()
// Start initial batch
for _ in 0..<min(maxConcurrent, urls.count) {
if let url = iterator.next() {
group.addTask {
try await self.processImage(at: url)
}
}
}
// As each completes, add another
for try await result in group {
results.append(result)
if let url = iterator.next() {
group.addTask {
try await self.processImage(at: url)
}
}
}
}
return results
}
```
</limited_concurrency>
<async_let>
```swift
// Concurrent bindings
func loadDashboard() async throws -> Dashboard {
async let user = fetchUser()
async let projects = fetchProjects()
async let notifications = fetchNotifications()
// All three run concurrently, await results together
return try await Dashboard(
user: user,
projects: projects,
notifications: notifications
)
}
```
</async_let>
</structured_concurrency>
<async_sequences>
<for_await>
```swift
// Iterate async sequence
func monitorChanges() async {
for await change in fileMonitor.changes {
await processChange(change)
}
}
// With notifications
func observeNotifications() async {
let notifications = NotificationCenter.default.notifications(named: .dataChanged)
for await notification in notifications {
guard !Task.isCancelled else { break }
await handleNotification(notification)
}
}
```
</for_await>
<custom_async_sequence>
```swift
struct CountdownSequence: AsyncSequence {
typealias Element = Int
let start: Int
struct AsyncIterator: AsyncIteratorProtocol {
var current: Int
mutating func next() async -> Int? {
guard current > 0 else { return nil }
try? await Task.sleep(for: .seconds(1))
defer { current -= 1 }
return current
}
}
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(current: start)
}
}
// Usage
for await count in CountdownSequence(start: 10) {
print(count)
}
```
</custom_async_sequence>
<async_stream>
```swift
// Bridge callback-based API
func fileChanges(at path: String) -> AsyncStream<FileChange> {
AsyncStream { continuation in
let monitor = FileMonitor(path: path) { change in
continuation.yield(change)
}
monitor.start()
continuation.onTermination = { _ in
monitor.stop()
}
}
}
// Throwing version
func networkEvents() -> AsyncThrowingStream<NetworkEvent, Error> {
AsyncThrowingStream { continuation in
let connection = NetworkConnection()
connection.onEvent = { event in
continuation.yield(event)
}
connection.onError = { error in
continuation.finish(throwing: error)
}
connection.onComplete = {
continuation.finish()
}
connection.start()
continuation.onTermination = { _ in
connection.cancel()
}
}
}
```
</async_stream>
</async_sequences>
<cancellation>
<checking_cancellation>
```swift
func processLargeDataset(_ items: [Item]) async throws -> [Result] {
var results: [Result] = []
for item in items {
// Check for cancellation
try Task.checkCancellation()
// Or check without throwing
if Task.isCancelled {
break
}
let result = await process(item)
results.append(result)
}
return results
}
```
</checking_cancellation>
<cancellation_handlers>
```swift
func downloadFile(_ url: URL) async throws -> Data {
let task = URLSession.shared.dataTask(with: url)
return try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
task.completionHandler = { data, _, error in
if let error = error {
continuation.resume(throwing: error)
} else if let data = data {
continuation.resume(returning: data)
}
}
task.resume()
}
} onCancel: {
task.cancel()
}
}
```
</cancellation_handlers>
<task_cancellation>
```swift
class ViewModel {
private var loadTask: Task<Void, Never>?
func load() {
// Cancel previous load
loadTask?.cancel()
loadTask = Task {
await performLoad()
}
}
func cancel() {
loadTask?.cancel()
loadTask = nil
}
deinit {
loadTask?.cancel()
}
}
```
</task_cancellation>
</cancellation>
<sendable>
<sendable_types>
```swift
// Value types are Sendable by default if all properties are Sendable
struct Item: Sendable {
let id: UUID
let name: String
let count: Int
}
// Classes must be explicitly Sendable
final class ImmutableConfig: Sendable {
let apiKey: String
let baseURL: URL
init(apiKey: String, baseURL: URL) {
self.apiKey = apiKey
self.baseURL = baseURL
}
}
// Actors are automatically Sendable
actor Counter: Sendable {
var count = 0
}
// Mark as @unchecked Sendable when you manage thread safety yourself
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return storage[key]
}
}
```
</sendable_types>
<sending_closures>
```swift
// Closures that cross actor boundaries must be @Sendable
func processInBackground(work: @Sendable @escaping () async -> Void) {
Task.detached {
await work()
}
}
// Capture only Sendable values
let items = items // Must be Sendable
Task {
await process(items)
}
```
</sending_closures>
</sendable>
<best_practices>
<do>
- Use `.task` modifier for view-related async work
- Use actors for shared mutable state
- Mark UI-updating code with `@MainActor`
- Check `Task.isCancelled` in long operations
- Use structured concurrency (task groups, async let) over unstructured
- Cancel tasks when no longer needed
</do>
<avoid>
- Creating detached tasks unnecessarily (loses structured concurrency benefits)
- Blocking actors with synchronous work
- Ignoring cancellation in long-running operations
- Passing non-Sendable types across actor boundaries
- Using `DispatchQueue` when async/await works
</avoid>
</best_practices>