iOS Development

What are Sendable and @Sendable closures in Swift? – Donny Wals

What are Sendable and @Sendable closures in Swift? – Donny Wals
Written by admin


Printed on: September 13, 2022

One of many targets of the Swift crew with Swift’s concurrency options is to offer a mannequin that permits developer to write down protected code by default. Because of this there’s lots of time and power invested into ensuring that the Swift compiler helps builders detect, and forestall entire lessons of bugs and concurrency points altogether.

One of many options that helps you forestall knowledge races (a typical concurrency problem) comes within the type of actors which I’ve written about earlier than.

Whereas actors are nice if you wish to synchronize entry to some mutable state, they don’t remedy each potential problem you may need in concurrent code.

On this submit, we’re going to take a more in-depth have a look at the Sendable protocol, and the @Sendable annotation for closures. By the top of this submit, it is best to have a superb understanding of the issues that Sendable (and @Sendable) intention to resolve, how they work, and the way you need to use them in your code.

Understanding the issues solved by Sendable

One of many trickiest points of a concurrent program is to make sure knowledge consistency. Or in different phrases, thread security. Once we move situations of lessons or structs, enum circumstances, and even closures round in an utility that doesn’t do a lot concurrent work, we don’t want to fret about thread security rather a lot. In apps that don’t actually carry out concurrent work, it’s unlikely that two duties try to entry and / or mutate a bit of state at the very same time. (However not unattainable)

For instance, you is likely to be grabbing knowledge from the community, after which passing the obtained knowledge round to a few capabilities in your most important thread.

Because of the nature of the primary thread, you’ll be able to safely assume that your whole code runs sequentially, and no two processes in your utility will likely be engaged on the identical referencea on the similar time, probably creating a knowledge race.

To briefly outline a knowledge race, it’s when two or extra components of your code try to entry the identical knowledge in reminiscence, and no less than one in every of these accesses is a write motion. When this occurs, you’ll be able to by no means be sure in regards to the order wherein the reads and writes occur, and you may even run into crashes for unhealthy reminiscence accesses. All in all, knowledge races aren’t any enjoyable.

Whereas actors are a improbable technique to construct objects that accurately isolate and synchronize entry to their mutable state, they’ll’t remedy all of our knowledge races. And extra importantly, it won’t be cheap so that you can rewrite your whole code to utilize actors.

Think about one thing like the next code:

class FormatterCache {
    var formatters = [String: DateFormatter]()

    func formatter(for format: String) -> DateFormatter {
        if let formatter = formatters[format] {
            return formatter
        }

        let formatter = DateFormatter()
        formatter.dateFormat = format
        formatters[format] = formatter

        return formatter
    }
}

func performWork() async {
    let cache = FormatterCache()
    let possibleFormatters = ["YYYYMMDD", "YYYY", "YYYY-MM-DD"]

    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<10 {
            group.addTask {
                let format = possibleFormatters.randomElement()!
                let formatter = cache.formatter(for: format)
            }
        }
    }
}

On first look, this code won’t look too unhealthy. We’ve a category that acts as a easy cache for date formatters, and we have now a activity group that can run a bunch of code in parallel. Every activity will seize a random date format from the listing of potential format and asks the cache for a date formatter.

Ideally, we anticipate the formatter cache to solely create one date formatter for every date format, and return a cached formatter after a formatter has been created.

Nevertheless, as a result of our duties run in parallel there’s an opportunity for knowledge races right here. One fast repair can be to make our FormatterCache an actor and this is able to remedy our potential knowledge race. Whereas that may be a superb answer (and truly the most effective answer should you ask me) the compiler tells us one thing else after we attempt to compile the code above:

Seize of ‘cache’ with non-sendable kind ‘FormatterCache’ in a @Sendable closure

This warning is making an attempt to inform us that we’re doing one thing that’s probably harmful. We’re capturing a price that can not be safely handed via concurrency boundaries in a closure that’s purported to be safely handed via concurrency boundaries.

With the ability to be safely handed via concurrency boundaries primarily implies that a price could be safely accessed and mutated from a number of duties concurrently with out inflicting knowledge races. Swift makes use of the Sendable protocol and the @Sendable annotation to speak this thread-safety requirement to the compiler, and the compiler can then test whether or not an object is certainly Sendable by assembly the Sendable necessities.

What these necessities are precisely will fluctuate a bit relying on the kind of objects you cope with. For instance, actor objects are Sendable by default as a result of they’ve knowledge security built-in.

Let’s check out different kinds of objects to see what their Sendable necessities are precisely.

Sendable and worth varieties

In Swift, worth varieties present lots of thread security out of the field. Whenever you move a price kind from one place to the subsequent, a duplicate is created which implies that every place that holds a duplicate of your worth kind can freely mutate its copy with out affecting different components of the code.

This an enormous advantage of structs over lessons as a result of they permit use to purpose regionally about our code with out having to contemplate whether or not different components of our code have a reference to the identical occasion of our object.

Due to this conduct, worth varieties like structs an enums are Sendable by default so long as all of their members are additionally Sendable.

Let’s have a look at an instance:

// This struct isn't sendable
struct Film {
    non-public let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "YYYY"
        return formatter
    }()

    let releaseDate = Date()
    var formattedReleaseDate: String {
        dateFormatter.string(from: releaseDate)
    }
}

// This struct is sendable
struct Film {
    var formattedReleaseDate = "2022"
}

I do know that this instance is a bit bizarre; they don’t have the very same performance however that’s not the purpose.

The purpose is that the primary struct does probably not maintain mutable state; all of its properties are both constants, or they’re computed properties. Nevertheless, Date and DateFormatter each are lessons they usually aren’t Sendable. Since our Film struct doesn’t maintain a duplicate of the Date or DateFormatter, all copies of Film can be trying on the similar situations of the Date and DateFormatter, which implies that we is likely to be taking a look at knowledge races if a number of Film copies would try to, for instance, set a special dateFormat on the DateFormatter.

The second struct solely holds Sendable state. String is Sendable and because it’s the one property outlined on Film, film can also be Sendable.

The rule right here is that each one worth varieties are Sendable so long as their members are additionally Sendable.

Sendable and lessons

Whereas each structs and actors are implicitly Sendable, lessons will not be. That’s as a result of lessons are rather a lot much less protected by their nature; all people that receives an occasion of a category truly receives a reference to that occasion. Because of this a number of locations in your code maintain a reference to the very same reminiscence location and all mutations you make on a category occasion are shared amongst all people that holds a reference to that class occasion.

That doesn’t imply we will’t make our lessons Sendable, it simply implies that we have to add the conformance manually, and manually be sure that our lessons are literally Sendable.

We will make our lessons Sendable by including conformance to the Sendable protocol:

remaining class Film: Sendable {
    let formattedReleaseDate = "2022"
}

The necessities for a category to be Sendable are just like these for a struct.

For instance, a category can solely be Sendable if all of its members are Sendable. Because of this they need to both be Sendable lessons, worth varieties, or actors. This requirement is an identical to the necessities for Sendable structs.

Along with this requirement, your class have to be remaining. Inheritance may break your Sendable conformance if a subclass provides incompatible overrides or options. For that reason, solely remaining lessons could be made Sendable.

Lastly, your Sendable class mustn’t maintain any mutable state. Mutable state would imply that a number of duties can try to mutate your state, main to a knowledge race.

Nevertheless, there are situations the place we’d know a category or struct is protected to be handed throughout concurrency boundaries even when the compiler can’t proof it.

In these circumstances, we will fall again on unchecked Sendable conformance.

Unchecked Sendable conformance

Whenever you’re working with codebases that predate Swift Concurrency, chances are high that you just’re slowly working your means via your app with a view to introduce concurrency options. Because of this a few of your objects might want to work in your async code, in addition to in your sync code. Because of this utilizing actor to isolate mutable state in a reference kind won’t work so that you’re caught with a category that may’t conform to Sendable. For instance, you may need one thing like the next code:

class FormatterCache {
    privatevar formatters = [String: DateFormatter]()
    non-public let queue = DispatchQueue(label: "com.dw.FormatterCache.(UUID().uuidString)")

    func formatter(for format: String) -> DateFormatter {
        return queue.sync {
            if let formatter = formatters[format] {
                return formatter
            }

            let formatter = DateFormatter()
            formatter.dateFormat = format
            formatters[format] = formatter

            return formatter
        }
    }
}

This formatter cache makes use of a serial queue to make sure synchronized entry to its formatters dictionary. Whereas the implementation isn’t very best (we may very well be utilizing a barrier or possibly even a plain previous lock as a substitute), it really works. Nevertheless, we will’t add Sendable conformance to our class as a result of formatters isn’t Sendable.

To repair this, we will add @unchecked Sendable conformance to our FormatterCache:

class FormatterCache: @unchecked Sendable {
    // implementation unchanged
}

By including this @unchecked Sendable we’re instructing the compiler to imagine that our FormatterCache is Sendable even when it doesn’t meet all the necessities.

Having this characteristic in our toolbox is extremely helpful if you’re slowly phasing Swift Concurrency into an current mission, however you’ll wish to assume twice, or possibly even thrice, if you’re reaching for @unchecked Sendable. It is best to solely use this characteristic if you’re actually sure that your code is definitely protected for use in a concurrent surroundings.

Utilizing @Sendable on closures

There’s one final place the place Sendable comes into play and that’s on capabilities and closures.

A lot of closures in Swift Concurrency are annotated with the @Sendable annotation. For instance, right here’s what the declaration for TaskGroup‘s addTask appears like:

public mutating func addTask(precedence: TaskPriority? = nil, operation: @escaping @Sendable () async -> ChildTaskResult)

The operation closure that’s handed to addTask is marked with @Sendable. Because of this any state that the closure captures should be Sendable as a result of the closure is likely to be handed throughout concurrency boundaries.

In different phrases, this closure will run in a concurrent method so we wish to ensure that we’re not by chance introducing a knowledge race. If all state captured by the closure is Sendable, then we all know for positive that the closure itself is Sendable. Or in different phrases, we all know that the closure can safely be handed round in a concurrent surroundings.

Tip: to study extra about closures in Swift, check out my submit that explains closures in nice element.

Abstract

On this submit, you’ve discovered in regards to the Sendable and @Sendable options of Swift Concurrency. You discovered why concurrent applications require further security round mutable state, and state that’s handed throughout concurrency boundaries with a view to keep away from knowledge races.

You discovered that structs are implicitly Sendable if all of their members are Sendable. You additionally discovered that lessons could be made Sendable so long as they’re remaining, and so long as all of their members are additionally Sendable.

Lastly, you discovered that the @Sendable annotation for closures helps the compiler be sure that all state captured in a closure is Sendable and that it’s protected to name that closure in a concurrent context.

I hope you’ve loved this submit. When you have any questions, suggestions, or recommendations to assist me enhance the reference then be at liberty to achieve out to me on Twitter.



About the author

admin

Leave a Comment