Home Articles

Strict Concurrency in Swift 6 - Part 1

Swift Concurrency

Swift Concurrency

Are you wondering about what is Strict Concurrency in Swift 6, and how to prepare your apps for the same? And want to expertise/understand your knowledge on concurrency in Swift, then this series of articles is for you!

If you are already aware of the SwiftConcurrency and interested only in StrictConcurrency in Swift6 and Data Isolation concepts please switch to the part 2 directly here. (TBA)

Whats the need of SwiftConcurrency?

The main concurrency hurdles we had before swift concurrency were:

🚫 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

So the language was in need of creating a new concurrency framework to overcome these, that’s where the SwiftConcurrency comes in.

Swift 6 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

What it includes?

βœ… 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

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

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

When you are using Actor’s you need to aware of one of the pitfalls of Actor which is 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

If you are interested in Actor Reentrancy more, you can have a detailed view on here

Swift Actor Pitfall by Lee Kah Seng

SPONSOR

Swift Logo

The World's Northernmost iOS Conference

This is the first ever World's Northernmost Apple Developers' Conference with One day of workshops, two days of tech-talks, Sauna πŸ§–, ice swimming 🏊, northern lights πŸ—», and drinks to help you forget about work and connect with like-minded professionals, it is organised byΒ Jesse Sipola

Get your tickets!

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

If you want to understand more on Sendable, I highly recommend Tim’s talk on Sendable here

Deep Dive into Sendable - Tim Condon


We will look into the Concept of Isolation in the next part.

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.