Skip to main content

Working with Modern Concurrency

1. Using async/await Keywords

Feature Description Example
async Marks a function as asynchronous, allowing it to pause and resume execution. func fetchData() async -> Data
await Pauses execution until the asynchronous operation completes. let data = await fetchData()
Task Represents an asynchronous operation that can be scheduled for execution. Task { await doSomething() }

Example: Basic async/await usage

func fetchUserData() async -> String {
    let url = URL(string: "https://api.example.com/user")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? ""
}

func main() async {
    let userData = await fetchUserData()
    print(userData)
}

Example: Concurrent execution with Task

func fetchImage() async -> Data {
    let url = URL(string: "https://api.example.com/image")!
    return try await URLSession.shared.data(from: url).0
}

func fetchText() async -> String {
    let url = URL(string: "https://api.example.com/text")!
    return try await URLSession.shared.data(from: url).0
}

func main() async {
    let task1 = Task { await fetchImage() }
    let task2 = Task { await fetchText() }
    
    let image = await task1.value
    let text = await task2.value
}

2. Defining Async Functions

Function Type Declaration Description
Async Function func asyncFunction() async throws -> ResultType Defines an asynchronous function that can await other async operations and may throw errors.
Async Function with Parameters func asyncFunction(param: String) async throws -> ResultType Defines an async function that accepts parameters and can await other operations.
Async Void Function func asyncVoidFunction() async Defines an async function that returns nothing.

Example: Defining an Async Function

func fetchUserData(from userId: String) async throws -> [String: Any] {
    let url = URL(string: "https://api.example.com/users/\(userId)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONSerialization.jsonObject(with: data) as! [String: Any]
}
Note: The async keyword in function signature indicates that the function is asynchronous and can be awaited.

3. Using Task and Task Groups

Feature Description Code Example
Task Represents an asynchronous operation that can be cancelled or awaited.
Task {
    await doSomething()
}
TaskGroup Allows managing multiple tasks as a group, useful for concurrent operations.
let group = TaskGroup<String, Error>()
group.addTask {
    return await fetchUser()
}
let results = await group.reduce(into: [String]()) { $0.append($1) }
Note: Task groups are ideal for managing a variable number of concurrent tasks.
Warning: Avoid nesting too many tasks to prevent resource exhaustion.

Task Lifecycle

  • Created with Task { ... }
  • Can be cancelled using Task.cancel()
  • Use TaskGroup for managing multiple concurrent tasks

4. Understanding Structured Concurrency

Structured concurrency is a paradigm that ensures safe and predictable concurrent execution in Swift. It enforces that all tasks spawned within a structured scope are properly managed and awaited, preventing issues like race conditions or unhandled errors.

Concept Description Notes
Task Task<Result, Error> Represents an asynchronous operation that can be awaited
TaskGroup TaskGroup<Result, Error> Allows spawning multiple tasks within a structured scope
withThrowingTaskGroup withThrowingTaskGroup<Result>(...) -> Result Initializes and runs a task group that can throw errors

Example: Basic Task Creation

import Foundation

func fetchData() async throws -> String {
    try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
    return "Fetched data"
}

Task {
    do {
        let result = try await fetchData()
        print(result)
    } catch {
        print("Error: \(error)")
    }
}

Example: Using TaskGroup

import Foundation

func fetchItem(_ id: Int) async throws -> String {
    try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
    return "Item \(id)"
}

func fetchItems() async throws -> [String] {
    return try await withThrowingTaskGroup(of: String.self) { group in
        var results: [String] = []
        
        for i in 1...3 {
            group.addTask {
                return try await fetchItem(i)
            }
        }
        
        for await result in group {
            results.append(result)
        }
        
        return results
    }
}

Task {
    do {
        let items = try await fetchItems()
        print(items)
    } catch {
        print("Error: \(error)")
    }
}
Note: Structured concurrency ensures that all tasks within a scope are awaited or cancelled before the scope ends, preventing resource leaks and unhandled errors.

5. Using Async Sequences

Async sequences in Swift provide a way to handle asynchronous operations that emit values over time. They are particularly useful for handling streams of data, such as network responses or user interactions.

Property Description Usage
AsyncSequence Protocol defining a sequence of values that are asynchronously produced. struct MyAsyncSequence: AsyncSequence { ... }
AsyncStream Concrete implementation of AsyncSequence that can be used to emit values asynchronously. let stream = AsyncStream<String> { continuation in ... }
AsyncThrowingStream AsyncSequence that can throw errors during emission. let stream = AsyncThrowingStream<String, Error> { continuation in ... }

Example: Creating and Using AsyncStream

import Foundation

let stream = AsyncStream<String> { continuation in
    DispatchQueue.global().async {
        for i in 1...5 {
            let value = "Value \(i)"
            continuation.yield(value)
            Thread.sleep(forTimeInterval: 0.5)
        }
        continuation.finish()
    }
}

Task {
    for await value in stream {
        print(value)
    }
}

Example: Using AsyncSequence with for-await-in Loop

import Foundation

func fetchAsyncData() -> AsyncStream<String> {
    return AsyncStream { continuation in
        Task {
            for i in 1...3 {
                let data = "Data \(i)"
                continuation.yield(data)
                try? await Task.sleep(nanoseconds: 1_000_000_000)
            }
            continuation.finish()
        }
    }
}

Task {
    for await data in fetchAsyncData() {
        print("Received: \(data)")
    }
}
Note: Async sequences are particularly useful when working with real-time data sources or when building reactive systems in Swift.

6. Using Async Streams

Async streams in Swift provide a way to handle sequences of values asynchronously, allowing you to process data as it becomes available without blocking the main thread.

Property Description Usage
AsyncStream<Element> Represents a stream of asynchronous values let stream = AsyncStream<String> { continuation in ... }
AsyncStream<Element>.Iterator Iterator for consuming values from the stream for await value in stream { ... }
AsyncStream<Element>.Continuation Used to send values into the stream continuation.yield("value")

Example: Creating and Consuming an Async Stream

import Foundation

func createAsyncStream() -> AsyncStream<Int> {
    return AsyncStream<Int> { continuation in
        for i in 1...5 {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) {
                continuation.yield(i * 10)
            }
        }
        continuation.finish()
    }
}

Task {
    for await value in createAsyncStream() {
        print("Received: \\(value)")
    }
}
Note: The AsyncStream is useful for scenarios where you want to emit values asynchronously over time, such as handling real-time data or user input.
Warning: Ensure that you call continuation.finish() to signal the end of the stream, otherwise consumers may wait indefinitely.

Key Points

  • AsyncStream allows asynchronous value emission.
  • Use continuation.yield() to send values into the stream.
  • Call continuation.finish() to signal completion.
  • Consumers can iterate over the stream using for await.

7. Implementing Actors

Feature Description Notes
actor actor MyActor { ... } Defines an actor type that encapsulates state and enforces thread safety
isolation Implicitly isolated by default Prevents data races through compile-time checks
async/await await actor.method() Used to call methods on actors asynchronously

Example: Basic Actor Implementation

actor Counter {
    private var count = 0

    func increment() {
        count += 1
    }

    func getValue() -> Int {
        return count
    }
}

// Usage
let counter = Counter()
Task {
    await counter.increment()
    print(await counter.getValue())
}

Example: Actor with Async Methods

actor DataFetcher {
    private var cache: [String: String] = [:]

    func fetchData(from url: String) async throws -> String {
        if let cached = cache[url] {
            return cached
        }

        // Simulate network fetch
        let data = "Fetched data from \(url)"
        cache[url] = data
        return data
    }
}

// Usage
let fetcher = DataFetcher()
Task {
    do {
        let data = try await fetcher.fetchData(from: "https://api.example.com")
        print(data)
    } catch {
        print("Error fetching data: \(error)")
    }
}
Note: Actors are isolated, meaning that their state is protected from data races. You must use await when calling methods on actors.
Warning: Never pass an actor instance to a non-actor context directly, as it may lead to data races.

8. Using MainActor for UI Updates

The MainActor is a special actor in Swift that ensures code runs on the main thread, which is essential for UI updates in iOS applications.

Property Description Usage
MainActor Represents the main thread in Swift concurrency Used to ensure UI updates happen on the main thread

Example: Updating UI on MainActor

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                Task {
                    await updateCounter()
                }
            }
        }
    }
    
    @MainActor
    func updateCounter() {
        count += 1
    }
}
Note: When using Task, ensure that any UI updates are performed on the main thread by marking the function with @MainActor.

9. Understanding Sendable Protocol

Property Description Usage
Sendable Marker protocol indicating a type is safe to send between actors Mark types that are thread-safe or immutable as Sendable
actor isolation Ensures that only one actor can access a type at a time Prevents data races in concurrent code

Example: Sendable Protocol Usage

struct User: Sendable {
    let name: String
    let id: Int
}

actor UserManager {
    private var users: [User] = []
    
    func addUser(_ user: User) {
        users.append(user)
    }
}
Note: Types conforming to Sendable can be safely passed between actors without explicit synchronization.
Warning: Marking a type as Sendable incorrectly can lead to data races and undefined behavior.
Property Description Notes
Sendable conformance Types that are immutable or thread-safe can conform to Sendable Includes value types with Sendable properties
Actor-isolated types Types that are only used within a specific actor context Not Sendable by default, must be explicitly marked if safe to share

10. Handling Cancellation

Method Description Notes
Task.cancel() Requests cancellation of the task Does not immediately stop execution
Task.isCancelled Boolean indicating if task was cancelled Can be checked within task to handle cleanup
withTaskCancellationHandler Execute code only if task is not cancelled Useful for cleanup or finalization logic

Example: Canceling a Task

let task = Task {
    for i in 0..<1000 {
        if Task.isCancelled { break }
        print("Processing item \(i)")
        try? await Task.sleep(nanoseconds: 1_000_000)
    }
}

task.cancel()

Example: Using withTaskCancellationHandler

let task = Task {
    withTaskCancellationHandler {
        print("Task is not cancelled, proceeding")
    } operation: {
        try await Task.sleep(nanoseconds: 5_000_000)
    }
}

11. Using Continuation

Property Description Usage
withCheckedContinuation Creates a checked continuation for asynchronous operations withCheckedContinuation { (continuation: CheckedContinuation<Result, Error>) in ... }
resume Resumes the continuation with a result or error continuation.resume(returning: value)
resume(throwing:) Resumes the continuation with an error continuation.resume(throwing: error)

Example: Using Continuation to Wrap Async Operation

func fetchData() async throws -> String {
    return try await withCheckedContinuation { continuation in
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            continuation.resume(returning: "Data fetched successfully")
        }
    }
}
Note: withCheckedContinuation ensures that the continuation is resumed exactly once and prevents misuse.

12. Using Task Priorities

Priority Description Usage
.low Lowest priority task Task(priority: .low) { ... }
.medium Medium priority task Task(priority: .medium) { ... }
.high High priority task Task(priority: .high) { ... }
.userInitiated Task initiated by user action Task(priority: .userInitiated) { ... }
.background Background task with lowest priority Task(priority: .background) { ... }

Example: Setting Task Priority

Task(priority: .high) {
    // High priority task
    print("Running high priority task")
}
Note: Task priorities influence scheduling but do not guarantee execution order or completion time.

Was this article helpful?