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]
}
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. | |
| TaskGroup | Allows managing multiple tasks as a group, useful for concurrent operations. | |
Task Lifecycle
- Created with
Task { ... } - Can be cancelled using
Task.cancel() - Use
TaskGroupfor 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)")
}
}
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)")
}
}
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)")
}
}
AsyncStream is useful for scenarios where you want to emit values asynchronously over time, such as handling real-time data or user input.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)")
}
}
await when calling methods on actors.
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
}
}
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)
}
}
Sendable can be safely passed between actors without explicit synchronization.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")
}
}
}
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")
}