This article continues with Swift 5.5 New Features (Part 1) to introduce new features related to Swift concurrency

TaskGroup

For more complex concurrent tasks, you can create taskgroups, which are collections of tasks that work together.

To reduce the risk of using task groups incorrectly, Swift does not provide a public constructor for task groups, which are created through functions like withTaskGroup(). We put the task to be performed in the function’s trailing closure, which provides us with the TaskGroup instance. With the async() method of this instance, we can start a task immediately.

Example code is as follows:

func printMessage(a) async {
    let string = await withTaskGroup(of: String.self) { group -> String in
        group.async { "Hello" }
        group.async { "From" }
        group.async { "A" }
        group.async { "Task" }
        group.async { "Group" }

        var collected = [String] ()for await value in group {
            collected.append(value)
        }

        return collected.joined(separator: "")}print(string)
}
Copy the code

Tip: You can use any function in an async call, as long as you make sure that all tasks in the task group return the same type of data. For some complex work, we may need to return enumerations with associated values. We can also introduce async let binding to solve this situation.

If the code in the task group can throw errors, we can either handle them directly within the group or we can have them thrown up for processing outside the group. Alternatively, use the withThrowingTaskGroup() function. If all possible errors are not caught, the function is called with a try.

For example, the following code demonstrates reading the weather index for multiple locations within a group and returning the average:

func printAllWeatherReadings(a) async {
    do {
        print("Calculating average weather...")

        let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in
            group.async {
                try await getWeatherReadings(for: "London")
            }

            group.async {
                try await getWeatherReadings(for: "Rome")
            }

            group.async {
                try await getWeatherReadings(for: "San Francisco")}// Flatten all arrays into a single array
            let allValues = try await group.reduce([], +)

            // Calculate the average value
            let average = allValues.reduce(0.+) / Double(allValues.count)
            return "The average temperature is zero\(average)"
        }

        print("Done!\(result)")}catch {
        print("Error in calculating data")}}Copy the code

Task groups provide a cancelAll() method that cancels all existing tasks within a group, but subsequent async() calls continue to add tasks to the group. We can use asyncUnlessCancelled() to ensure that tasks are added only if the task group has not been cancelled.

async letThe binding

Se-0317 introduces the ability to create subtasks using the async let simple syntax. It’s a more convenient option than task groups when you’re dealing with multiple heterogeneous asynchronous return types.

To demonstrate the code, we create a structure with three different types of attributes, which are obtained by three asynchronous functions:

struct UserData {
    let username: String
    let friends: [String]
    let highScores: [Int]}func getUser(a) async -> String {
    "Taylor Swift"
}

func getHighScores(a) async- > [Int] {[42.23.16.15.8.4]}func getFriends(a) async- > [String] {["Eric"."Maeve"."Otis"]}Copy the code

The async let is simple to use — it runs asynchronously. We can make each property fetch an Async let, and then wait for all three properties to return for object creation.

func printUserDetails(a) async {
    async let username = getUser()
    async let scores = getHighScores()
    async let friends = getFriends()

    let user = await UserData(name: username, friends: friends, highScores: scores)
    print("My name is\(user.name)And I have\(user.friends.count)A friend!")}Copy the code

Important: Async let must be used in an asynchronous context, if you do not explicitly wait for the result of an async let, Swift will implicitly wait for it.

For functions that may throw an error, we do not need to use a try before the async let, since capture is deferred until the moment of await result. Likewise, the await keyword is implicit. So, when we use async let, we don’t need to write async let result = try await someFunction(), just write async let result = someFunction().

The demo code is as follows:

enum NumberError: Error {
    case outOfRange
}

func fibonacci(of number: Int) async throws -> Int {
    if number < 0 || number > 22 {
        throw NumberError.outOfRange
    }

    if number < 2 { return number }
    async let first = fibonacci(of: number - 2)
    async let second = fibonacci(of: number - 1)
    return try await first + second
}
Copy the code

In the above code, the Fibonacci (of:) call is implicitly equivalent to a try await Fibonacci (of:) call, but we defer it to a later try await processing.

Continue asynchronous tasks with synchronous code

The SE-0300 introduces a new function to help us adapt the old Completion Handler style API to modern asynchronous style code.

For example, the following function returns a value via a Completion handler:

func fetchLatestNews(completion: @escaping ([String]) -> Void) {
    DispatchQueue.main.async {
        completion(["The Swift for release 5.5."."Apple acquires Apollo"])}}Copy the code

If we want to use async/await, we may need to rewrite this function. In reality, we might not be able to override it for a variety of reasons — the function comes from an external library, for example.

Continuations allow us to bridge completion handlers and asynchronous functions, wrapping older code in a more modern API style. For example, the withCheckedContinuation() function can create a continuation in which you can run arbitrary code, The resume(returning:) method of a continuation receives the data in completion as a relay and converts it into the return value of an async-style function. Our wrapped async version of the function can then be used with await syntax in synchronous style:

func printNews(a) async {
    let items = await fetchLatestNews()

    for item in items {
        print(item)
    }
}
Copy the code

The term checked means that Swift checks our behavior at run time: are we calling Resume once or multiple times? This is important because continuations leak resources if we never continue, and multiple invocations run into problems.

To be clear, a continuation must resume only once.

In addition to providing run-time checking continuations, Swift also provides a withUnsafeContinuation function that works just the same as the withCheckedContinuation function that provides run-time checking.

Of course, the former must exist for a reason. When you have performance requirements, switch to the run-time checked but better performance version of withUnsafeContinuation when the code is actually implemented.

Actors

Se-0306 introduces actors, a new type that is conceptually similar to classes but can be safely used in parallel environments. The safe use of parallel environments is possible because Swift ensures that mutable state in actors can only be accessed by one thread at any one time, a mechanism that can eliminate serious bugs at the compiler level.

To illustrate the problem actors solve, let’s use the following code as an example. Imagine a class called RiskyCollector that can be used to trade cards with other Collectors.

class RiskyCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    func send(card selected: String.to person: RiskyCollector) -> Bool {
        guard deck.contains(selected) else { return false }

        deck.remove(selected)
        person.transfer(card: selected)
        return true
    }

    func transfer(card: String) {
        deck.insert(card)
    }
}
Copy the code

The above code is safe in a single-threaded environment: we check if the deck contains the selected card, remove it, and transfer it to another collector’s deck. However, in a multithreaded environment our code has potential race conditions.

If we call send(card:to:) more than once at the same time, the following might happen:

  1. The first thread checks that the card is in the deck and continues.

  2. The second thread also checks that the card is in the suit and continues.

  3. The first thread removes the target card from the deck and transfers it to someone else.

  4. The second thread also tries to remove the card from the deck, but the card is actually no longer there, but it is again transferred to someone else.

As mentioned above, there is a situation where one player is one card short and the other player is given two cards.

Actors solve this problem by introducing isolation: stored properties and methods in actors cannot be read outside the Actor object unless the code read is asynchronous; Writing is completely confined to the actor object. The asynchronous limitation of concurrent reads is not for performance reasons, but because Swift automatically queues read requests serially to avoid race conditions.

Therefore, we can rewrite the RiskyCollector class as a SafeCollector Actor like this:

actor SafeCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    func send(card selected: String.to person: SafeCollector) async -> Bool {
        guard deck.contains(selected) else { return false }

        deck.remove(selected)
        await person.transfer(card: selected)
        return true
    }

    func transfer(card: String) {
        deck.insert(card)
    }
}
Copy the code

There are a few things to note:

  1. Actors are through newactorCreated by keyword. This is a new type introduced by Swift 5.5, andstruct.class.eunmTo coexist.
  2. send()The method is marked asasyncThis is because it is suspended until the transfer action is complete.
  3. althoughtransfer(card:)Methods are not marked asasyncWe still need to useawaitThis is because the procedure needs to wait for something elseSafeCollectorActors handle requests. (Because the method receiver is not itself, but another actor)

Just to be clear, an actor is only free to use its own math and methods, asynchronously or not, while interacting with other actors is always done asynchronously. Based on these Settings, Swift ensures that all actor-isolated states are never accessed at the same time. Importantly, this is done at compile time, so concurrency is safe.

Actors and classes have the following similarities:

  • Both are reference types and therefore can be used to share state,
  • Both can have methods, attributes, constructors, and subscripts.
  • Both can follow the protocol and can be generic.
  • Both static properties and methods behave exactly the same, because static properties and methods don’tselfThe concept of data isolation is not involved.

Key differences between actors and classes:

  • Actors currently do not support inheritance, which makes their constructors simpler — no convenience constructors, overrides andfinalThe keyword. This may change in future versions of Swift.
  • All actors implicitly followActorProtocol. Other specific types cannot use this protocol.

The best description I’ve heard to explain the difference between actors and classes is “Actors deliver messages, not memory.” So instead of directly manipulating someone else’s properties and calling their methods, we pass messages asking someone else to change the data and let the Swift runtime take care of it.

Global actors

Se-0316 enables global state isolation with actors to avoid race conditions.

Although we theoretically use a lot of global actors, for now we mainly use @mainActor global actors to mark properties and methods that are only accessible in the main thread.

For example, we might have a class that deals specifically with data storage in our app. For security reasons, all writes to persistent storage are rejected if they are not on the main thread.

class OldDataController {
    func save(a) -> Bool {
        guard Thread.isMainThread else {
            return false
        }

        print("Saving the data...")
        return true}}Copy the code

The above code certainly works, but with @mainActor we can make sure that the save() method is always executed on the main thread, as if we were explicitly running it with dispatchqueue.main:

class NewDataController {
    @MainActor func save(a) {
        print("Saving the data...")}}Copy the code

Swift ensures that the save() method is always executed on the main thread.

Note: Since we are advancing work through actors, we must call save() with await, async let or similar asynchronous means.

@MainActor is actually a global actor wrapper for the MainActor structure.

Sendable@Sendableclosure

The SE-0302 provides support for “deliverable” data, which is data that can be safely moved to another thread. This new feature is implemented using the Sendable protocol and the @sendable function attribute.

There are many types that are naturally safe when sent across threads:

  • All Swift value types, includingBool.Int`, String, etc
  • Wrapper data is a selectable value type
  • A collection of standard libraries containing value types, for exampleArray<String>Or a Dictionary < Int, String > `
  • Elements are all tuples of value type
  • Metatype, for exampleString.self

All of these have been upgraded to follow the Sendable protocol.

For custom types, there are several cases:

  • Actors automatically followSendableBecause their synchronization is implemented within the type.
  • If only followSendable, then custom structs and enumerations are automatically followedSendable, this has to do withCodableWorks in a similar way.
  • Custom classes can be followed only if they meet these conditionsSendable: inherited fromNSObjectOr does not inherit from other classes; All properties are constant and followSendableThe class tofinalThe flag blocks itself from being inherited.

We use the @sendable attribute to mark functions that work in a concurrent environment, which enforces multiple rules to ensure that we don’t make mistakes. For example, the operation we pass to the Task constructor is marked @sendable, which means the operation is allowed only if the data captured by the Task is constant.

func printScore(a) async { 
    let score = 1

    Task { print(score) }
    Task { print(score) }
}
Copy the code

When score above is a variable, Task code blocks are not allowed because it is possible that one Task is accessing its value while another Task is modifying its value.

You can also mark your own functions and closures with @sendable, which enforces a similar rule for captured values:

func runLater(_ function: @escaping @Sendable() - >Void) -> Void {
    DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function)
}
Copy the code

For more articles, please pay attention to the wechat public number: Swift Garden