Concurrency in Swift
? Koushik Mudi

Concurrency in Swift

1. The Old Way & The New Way

The Old Way:

Before Swift 5.5, managing concurrency typically involved utilizing mechanisms such as Grand Central Dispatch (GCD) and closures.

This often resulted in callback-heavy code, making it challenging to follow and maintain.

After all, we all despise the callback hell, don’t we?


The New Way:

Swift 5.5 introduced a modernized concurrency model, featuring the async and await keywords. This paradigm shift enables developers to write asynchronous code in a more linear, synchronous-like fashion, vastly improving code readability and organization.


Here is a comparison between the two:

// Old way with closure

func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {

    DispatchQueue.global().async {

        // Asynchronous operation

        if let data = try? Data(contentsOf: URL(string: "https://example.com/data")!) {

            completion(.success(data))

        } else {

            completion(.failure(MyError.dataFetchingError))

        }

    }

}





// New way with async/await

func fetchData() async throws -> Data {

    let (data, response) = try await URLSession.shared.data(from: URL(string: "https://example.com/data")!)    

    return data

}        


2. Async/Await — Ditch Closures


Async/Await:

The introduction of async and await simplifies asynchronous code. The async keyword denotes a function as asynchronous, while await is used to pause the execution until the awaited operation completes.


Ditching Closures:

Async/await replaces closure-based patterns, making code more linear and eliminating the need for nesting our code.


// Using closures
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    networkService.fetchData { result in

        switch result {

        case .success(let data):

            self.processData(data: data, completion: completion)

        case .failure(let error):

            completion(.failure(error))
        }
    }
}

// Using async/await

func fetchData() async throws -> Data {

    let data = try await networkService.fetchData()

    return try processData(data: data)

}        


Now, if we had a few more closures inside networkService.fetchData { result in }, that would soon complicate things to a very uncomfortable level.


3. Closures and Async Interoperability — Continuation


Continuation:

Swift Concurrency introduces two powerful functions, withContinuation and withThrowingContinuation, that enable developers to work with continuations and throwing continuations respectively. These functions provide a structured and concise way to capture and manage the state of asynchronous tasks, allowing for efficient and responsive code execution. This document will delve into the details of withContinuation and withThrowingContinuation in Swift Concurrency.


Example of withContinuation:

func fetchData() async throws -> Data {

    return try await withContinuation { continuation in

        async {

            // Simulating an asynchronous network request

            await Task.sleep(2 * .seconds)

            

            if let data = try? Data(contentsOf: URL(string: "https://example.com/data")!) {

                continuation.resume(returning: data)

            } else {

                continuation.resume(throwing: NetworkError.failed)

            }

        }

    }

}        

Example of withThrowingContinuation:

func parseData(data: Data) async throws -> String {

    return try await withThrowingContinuation { continuation in

        async {

            // Simulating parsing data asynchronously

            await Task.sleep(1 * .seconds)

            

            if let parsedString = String(data: data, encoding: .utf8) {

                continuation.resume(returning: parsedString)

            } else {

                continuation.resume(throwing: ParsingError.invalidData)

            }

        }

    }

}        

The withContinuation and withThrowingContinuation functions in Swift Concurrency provide powerful mechanisms to capture and manage the state of asynchronous tasks. By encapsulating the logic within closures and working with continuations and throwing continuations respectively, developers can write clean and concise code to handle asynchronous operations. These functions enhance the error handling capabilities within the context of asynchronous code, ensuring efficient and responsive execution. Swift Concurrency empowers developers to create robust and scalable applications by providing a structured approach to concurrency.


4. Isolation

Swift Concurrency introduces new keywords and behaviors to ensure safe and efficient concurrent code execution. Isolation and the isolated and nonisolated keywords play a crucial role in managing access to mutable state and maintaining data integrity in concurrent environments. This document will explore the concepts of isolation, as well as the isolated and nonisolated keywords in Swift Concurrency, and their respective behaviors.


1. Isolation:

Isolation refers to the level of access control and protection applied to mutable state in concurrent code. It ensures that only one task can access mutable state at a time to avoid race conditions and data corruption. Swift Concurrency provides three levels of isolation: actor, nonisolated, and unsafe.


Key Points:

  • actor is the highest level of isolation and is represented by the actor keyword. Actors encapsulate mutable state and provide a clear boundary for accessing and modifying it. Only one task can access an actor's mutable state at a time, ensuring thread-safety.
  • nonisolated is the default level of isolation and allows access to non-actor types from within an actor. It ensures that non-actor types are accessed safely within the actor's isolation context.
  • unsafe is the lowest level of isolation and is used when manual synchronization is required. It allows multiple tasks to access mutable state simultaneously, potentially leading to race conditions and data corruption. It should be used with caution and only when absolutely necessary.


2. Isolated Keyword:

The isolated keyword in Swift Concurrency is used to explicitly specify the level of isolation for a particular function or property. It allows developers to control the access to mutable state and ensure safe concurrent execution.


Key Points:

  • The isolated keyword is used as an attribute on functions and properties to specify the level of isolation.
  • When applied to a function or property, the isolated keyword ensures that access to mutable state is synchronized according to the specified isolation level.
  • The isolated keyword can be used with different levels of isolation, such as isolated, nonisolated, or unsafe.


3. nonisolated Keyword:

The nonisolated keyword in Swift Concurrency is used to explicitly specify that a function or property is not isolated. It allows non-actor types to be accessed safely within an actor's isolation context.


Key Points:

  • The nonisolated keyword is used as an attribute on functions and properties to indicate that they can be safely accessed from within an actor.
  • When applied to a function or property, the nonisolated keyword ensures that it can be accessed safely within the actor's isolation context.
  • The nonisolated keyword is the default level of isolation for non-actor types.


Behavior Examples:

1. Actor Isolation:

actor MyActor {

    var count = 0

    

    func increment() {

        count += 1

    }

    

    nonisolated func printCount() {

        print(count)

    }

}



let actorInstance = MyActor()

await actorInstance.increment() // Accessing mutable state within an actor

await actorInstance.printCount() // Accessing nonisolated function from within an actor        


2. Non-Actor Isolation:

struct MyStruct {

    var value = 10

    nonisolated mutating func modifyValue() {

        value += 5

    }
}



let myStructInstance = MyStruct()

myStructInstance.modifyValue() // Accessing a nonisolated function from a non-actor context        

Isolation and the isolated and nonisolated keywords in Swift Concurrency provide mechanisms to manage access to mutable state and maintain data integrity in concurrent code. By using actors with different levels of isolation, developers can ensure thread-safety and avoid race conditions. The isolated and nonisolated keywords allow for explicit control over the access to mutable state, enabling safe concurrent execution. Swift Concurrency empowers developers to write scalable and robust concurrent code by providing clear and concise mechanisms for isolation and access control.


6. @Model Derivative

The @Model derivative simplifies the management of shared state by automatically conforming to actor isolation rules.

@Model
struct SharedData {

    var value: Int = 0

  // Automatically becomes an actor, ensuring safe concurrent access

    func increment() {
        value += 1
    }

}        


In the above example, the @Model annotation transforms SharedData into an actor, ensuring safe concurrent access to its properties and methods.


This has been an overview of Concurrency in Swift. To be detailed, each of the topics need their own scope. It's a vast area to explore and Apple's WWDC videos would be beneficial.

TAGS

#swift #concurrency #swift5 #swiftconcurrency #ios #macos #development #programming #iosprogramming #asynchronous

要查看或添加评论,请登录

Koushik M.的更多文章

  • Structured Concurrency in Swift: A Deep Dive

    Structured Concurrency in Swift: A Deep Dive

    After a weekend post on Concurrency, I figured it would be a great opportunity to talk about Structured Concurrency…

  • Swift: The Beauty of ResultBuilder

    Swift: The Beauty of ResultBuilder

    I wrote a post and said I'll write a detailed article. All because some small and lesser known features of Swift…

    1 条评论
  • Swift — Actors

    Swift — Actors

    What are Actors in Swift In SE-0306, it says An actor is a reference type that protects access to its mutable state…

  • Swift — Are Structs Safe?

    Swift — Are Structs Safe?

    Well, well! It's heard that structs are safe because they are value types, points to no references and are safe to use.…

  • Retain Cycle — Efficient Memory Management in Swift

    Retain Cycle — Efficient Memory Management in Swift

    Before we find the solution, we need to explore the problem. MEMORY When we talk about applications, we completely…

  • Swift Types – Simple & Complex

    Swift Types – Simple & Complex

    Overview I wrote about Swift Types a while ago in this post. So I thought I would explore and share a bit more and be…

  • Basics — Types & Type safety

    Basics — Types & Type safety

    Types – Obj-C & Swift Types or data structure, whatever they may be called, play a very important role in programming…

  • Save Time w/ This Library

    Save Time w/ This Library

    Tired of writing the same code for UI? Have a loads of work but rounding corners, adding shadows and what not eats up…

社区洞察

其他会员也浏览了