Home Articles

Strict Concurrency in Swift 6 - Part 1

Prepare to Say Goodbye to

🚫 Callback hell - Nested closures leading to unreadable, difficult-to-maintain code.
🚫 Error-Handling Headaches - Complex error propagation with callbacks or completion handlers.
🚫 Thread-Safety Issues - Manual thread management, prone to race conditions and bugs.
🚫 Complicated Synchronisation - Dispatch groups, semaphores, and complex logic to coordinate tasks.
🚫 Unreadable Asynchronous Code - Hard-to-follow, deeply nested code paths.

Embrace

βœ… Async/Await - Simple & Clean: Write async code that reads like sync code and alternative to nested closures.
βœ… Structured Concurrency - Task Management: Automatically manages task lifecycle and ensures tasks are properly completed or canceled.
βœ… Actors Isolation - Thread Safety: Prevents data race by isolating state and Only one task accesses mutable state at a time.
βœ… Tasks - Building Blocks: Create, manage, and cancel async operations easily and No need for manual thread management.
βœ… Cleaner Code - Less Boilerplate: Focus on logic, not threading issues and Async code is now as readable as sync code.
βœ… Strict Concurrency - Swift 6 further enhanced capabilities introducing stricter concurrency checks

Thanks to Swift 5.5 and Enhanced in Swift 6

Evolution of Concurrency APIs

🧡 NSThread - Manual thread management
πŸŒ€ GCD - Task management with queues
πŸ“‹ Operation Queues - Task dependencies and ordering
πŸ”— Combine - Reactive programming model
βœ… Swift 5 - Async/Await & Structured Concurrency
πŸ›‘οΈ Swift 6 - Complete Currency by default and Data Race Safety

Swift 6 Strict Concurrency Roadmap

First Phase

  • ⏳ Async/Await

  • πŸ“ Tasks and πŸ•ΈοΈ Structured Concurrency

  • 🎭 Actors and Actor Isolation - Foundation

  • πŸ”—Β Concurrency Interoperability with Objective-C

  • πŸ”„Β Async handlers

Second Phase

  • Full Isolation Enforcement

Swift Concurrency Roadmap

Swift Concurrency Roadmap

Async/Await, which simplify and streamline asynchronous programming, Structured Concurrency and Tasks, which bring order and safety to managing concurrency. Finally, we’ll dive into Actors, Sendable that ensures thread-safe operations.

Each topic will be covered with features, version history, pros and cons and practical examples.

Async/Await

Features

Basic Syntactic Building Block

πŸ“œ Suspension Points: Mark functions as async to enable the use of suspension points within the function.

⏳ Awaiting Results: The await keyword introduces a suspension point, where the function pauses until the asynchronous operation completes.

πŸ”„ Error Handling: Integrates seamlessly with do-catch for handling errors in asynchronous functions.

Flow Diagram

Async-Await Flow Diagram

Async-Await Flow Diagram

Practical Examples

Function that simulates an asynchronous task

// Define a function that simulates an asynchronous task
func performTask(taskNumber: Int) async {
    print("Task \(taskNumber) started.")

    do {
        try await Task.sleep(nanoseconds: 1_000_000_000) // Simulate some asynchronous work (1 second)
        print("Task \(taskNumber) completed.")
    } catch {
        print(error)
    }
}

Simple Example of Async/await

Networking Example with Generics

// Async-await Networking Example with Generics
func sendRequest<T>(urlStr: String) async throws -> T where T : Decodable {
    guard let urlStr = urlStr as String?, let url = URL(string: urlStr) as URL?else {
        throw NetworkError.invalidURL
    }
    
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode else {
        throw NetworkError.unexpectedStatusCode
    }
    
    guard let data = data as Data? else {
        throw NetworkError.unknown
    }
    
    guard let decodedResponse = try? JSONDecoder().decode(T.self, from: data) else {
        throw NetworkError.decode
    }
    
    return decodedResponse
}

Networking Example

Version History

Simplifying Asynchronous Programming

Streamlining Concurrency and Alternative to Callback/Closures

Introduced: iOS 15

Changes

  • Swift 5.5: Introduction of Async/Await - Proposal SE-0296

  • Swift 6: Continued improvements and complementary use with GCD, OperationQueue, and Combine

Pros

  • Readability, Error Handling and alternative to Completion Handler/Closures

  • Reduces complexity associated with Callback Hell or nested closures

  • Reduces chance of race conditions or deadlocks

Cons

  • Limited backward compatibility

  • Complex Integrations when interacting with existing GCD or OperationQueue

Meme about Callback Hell vs Async-Await

Meme about Callback Hell vs Async-Await

Tasks

Features

Fundamental Unit of Concurrent Work

πŸ“œ Sequential, Asynchronous and Self-contained

πŸ”„ Task Groups: Use TaskGroup to manage collections of tasks that run concurrently and complete as a group.

πŸ” Task Prioritisation: Assign priorities to tasks (e.g., .userInitiated, .background) to optimize performance.

πŸ“…Β Task Cancellation: Easily cancel tasks using the cancel() method, propagating cancellation to child tasks.

Task Priority

Task Priority

Practical Example

Task(priority: .high) {
    print("high Task Started")
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("high Task Completed")
}

Task(priority: .userInitiated) {
    print("userInitiated Task Started")
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("userInitiated Task Completed")
}

Task(priority: .medium) {
    print("medium Task Started")
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("medium Task Completed")
}

Task(priority: .low) {
    print("low Task Started")
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("low Task Completed")
}

Task(priority: .utility) {
    print("utility Task Started")
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("utility Task Completed")
}

Task(priority: .background) {
    print("Background Task Started")
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    print("Background Task Completed")
}

QoS Task

// Independent - Useful when you start the task that isn't tied to current context.
Task.detached {
    print("Detached Task started at \(Date())")
    
    try? await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    
    print("Detached Task completed at \(Date())")
}

Independent Task Execution

// Group Tasks
func stucturedConcurrencyWithTaskGroup() async {
    await withTaskGroup(of: String.self) { group in
        for i in 1...3 {
            group.addTask {
                try? await Task.sleep(nanoseconds: UInt64(i) * 1_000_000_000)
                return "Group Task \(i) completed"
            }
        }
        
        for await result in group {
            print(result)
        }
    }
}

Task {
    await stucturedConcurrencyWithTaskGroup()
}

Task Group Example

// Task Cancellation
func taskWithCancellation() async {
    let task = Task {
        for i in 1...3 {
            try? await Task.sleep(nanoseconds: 1_000_000_000)
            
            try await Task.checkCancellation()
            
            print("Task iteration \(i) completed")
        }
        
        return "Task completed"
    }
    
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        task.cancel()
    }
    
    do {
        let result = try await task.value
        print(result)
    } catch {
        print("Task was canceled")
    }
}

Task {
    await taskWithCancellation()
}

Thread.sleep(forTimeInterval: 7)

Task Cancellation Example

Version History

The Building Block of Swift Concurrency

Fundamental Unit of Concurrent Work

Introduced: iOS 15

Changes

  • Swift 5.5: Introduction of Tasks - Proposal SE-0304

  • Swift 6: Improvements in Task Management and Task Isolation

Pros

  • Built-in Cancellation

  • Works well with async-await

  • Reducing common concurrency issues such as Thread explosion and so on

Cons

  • Managing multiple tasks

  • Limited backward compatibility

  • Complex scenarios such as cancellation and task groups

Task vs DispatchQueue

Task vs DispatchQueue, credits: Swift Senpai

Task vs DispatchQueue, credits: Swift Senpai

Structured Concurrency

Features

Organised and Efficient Task Management

🧡 Thread Safety: Structured concurrency ensures that all spawned tasks are properly handled within their scope.

πŸ“ Task Lifespan Management: Automatically handle the lifecycle of tasks, cleaning up resources when tasks complete.

πŸ“‹ Error Propagation: Any error in child tasks is automatically propagated up to the parent task, ensuring robust error handling.

Flow Diagram

Structured Concurrency, credits: WWDC-21 Explore structured concurrency in Swift

Structured Concurrency, credits: WWDC-21 Explore structured concurrency in Swift

Practical Example

private let baseURL =  "https://picsum.photos/"

private(set) var images: [UIImage] = [UIImage]()

private func fetchImage(imageUrl: String) async throws ->  UIImage {
    do {
        guard let url = URL(string: baseURL + imageUrl) else {
            throw URLError(.badURL)
        }
        
        print("Val: \(imageUrl)")
        
        let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
        if let image = UIImage(data: data) {
            print(image)
            return image
        } else {
            throw URLError(.badURL)
        }
    } catch {
        if let error = error as? URLError, error.code == URLError.cancelled {
            print(error)
        } else {
            throw error
        }
    }
    
    return UIImage()
}

Method helps in Fetching Image from URL

func fetchMultipleImagesArray() async throws  {
    async let image100 = fetchImage(imageUrl:"100")
    async let image101 = fetchImage(imageUrl:"101")
    async let image102 = fetchImage(imageUrl:"102")
    async let image103 = fetchImage(imageUrl:"103")
    async let image104 = fetchImage(imageUrl:"104")

    let asyncLetResult = try await ([image100, image101, image102, image103, image104])

    await MainActor.run {
        images.append(contentsOf: asyncLetResult)
    }
}

Task {
    do {
        try await fetchMultipleImagesArray()
    } catch {
        print("Failed to download images: \(error)")
    }
}

async-let - Example of Structured Concurrency

/// Output:
Val: 104
Val: 101
Val: 103
Val: 100
Val: 102

Image: UIImage:0x6000030018c0 anonymous {104, 104} renderingMode=automatic(original)
Image: UIImage:0x600003001830 anonymous {101, 101} renderingMode=automatic(original)
Image: UIImage:0x600003009170 anonymous {102, 102} renderingMode=automatic(original)
Image: UIImage:0x600003009200 anonymous {100, 100} renderingMode=automatic(original)
Image: UIImage:0x6000030017a0 anonymous {103, 103} renderingMode=automatic(original)

Console output showing how async-let works

Version History

Organising Concurrent Tasks

Ensuring Safe and Predictable Task Management

Introduced: iOS 15

Changes

  • Swift 5.5: Introduction of Structured Concurrency - Proposal SE-0304

  • Swift 6: Enhanced support and refinements for structured concurrency.

Pros

  • Safety and Predictability

  • Integration with Async/Await, Async-let - Proposal SE-0317

  • Error Handling and Isolation of concurrent operations

Cons

  • Limited backward compatibility

  • Complexity in Large codebases

Actors

Features

Coordination and Safety in Concurrency

🧩 Task Coordination: Orchestrate/Coordinate multiple tasks with controlled access to shared data.

πŸ”’ Data Isolation: Isolates internal state, allowing only one task at a time to modify it.

πŸ›‘ Concurrency Safety: Prevents data races by managing concurrent mutations effectively.

Practical Example

Class vs Actors

class Counter {
    var value = 0
    
    func increment() -> Int {
        let currentValue = value
        
        Thread.sleep(forTimeInterval: 0.1)
        
        value = currentValue + 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print("Class 🎾 \(counter.increment())")
}

Task.detached {
    print("Class πŸ₯Ž \(counter.increment())")
}

A Counter Class

actor SafeCounter {
    var value = 0
    
    func increment() -> Int {
        let currentValue = value
        
        Thread.sleep(forTimeInterval: 0.1)
        
        value = currentValue + 1
        return value
    }
}

let safeCounter = SafeCounter()

Task.detached {
    print("Actor ⚽️ \(await safeCounter.increment())")
}

Task.detached {
    print("Actor πŸ€ \(await safeCounter.increment())")
}

A Counter Actor

Features

Safeguarding Shared State

Isolating State to Prevent Data Races

Introduced: iOS 15

Changes

  • Swift 5: Not available

  • Swift 5.5: Introduction of Actors - Proposal SE-0306

Pros

  • Data safety and simplicity

  • Reference type that helps in concurrency environment

  • Integration with async/await and Task and helps in Isolation

Cons

  • Actor Reentrancy

  • Limited backward compatibility

  • Complexity in Large codebases

Actor Reentrancy

  • An actor is suspended during a task

  • Another task enters the actor while the first task is suspended

  • Causes by actor’s reentrant behaviour

Actor Reentrancy

Actor Reentrancy

Sendable

Features

A thread-safe type whose values can be shared across arbitrary concurrent contexts without introducing a risk of data races.

Sendable and Sendable Closures SE-0302

Sendable == Thread Safety

  • Introduced in Swift 5.7

  • Sendable is just a Protocol

  • Most base types of Swift Foundation are Sendable

Swift Library

Swift Foundation - Date confirming to Sendable

Swift Foundation - Date confirming to Sendable

SE Proposal

Sendable Proposal - SE-0302

Sendable Proposal - SE-0302

Practical Example

Value Types

/// Value Types
struct User: Codable, Sendable {
    let name: String
    let age: Int
}

Structure - Sendable

/// Functions and Closures- @Sendable
func fetchData(endPoint: String, resultHandler: @Sendable @escaping (Result<User, Error>) -> Void) {
    resultHandler(.success(User(name: "Saba", age: 1)))
}

Functions and Closures - Sendable Closures

/// Reference types that internally manage access to their state
actor Account {
    private var balance: Int = 0
    
    func deposit(amount: Int) {
        balance += amount
    }
    
    func getBalance() -> Int {
        return balance
    }
}

Actor - Reference Type - Sendable

/// Reference types with no mutable storage
final class Address: Sendable {
    let houseNo: Int
    let streetName: String
    let cityName: String

    init(houseNo: Int, streetName: String, cityName: String) {
        self.houseNo = houseNo
        self.streetName = streetName
        self.cityName = cityName
    }
}

Reference Type - Class(final) - Sendable

We will look into the Isolation in the upcoming article…

This is a free third party commenting service we are using for you, which needs you to sign in to post a comment, but the good bit is you can stay anonymous while commenting.