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


Printed on: September 13, 2022

One of many targets of the Swift crew with Swift’s concurrency options is to offer a mannequin that enables developer to put in writing protected code by default. Which means that there’s loads of time and vitality invested into ensuring that the Swift compiler helps builders detect, and forestall complete lessons of bugs and concurrency points altogether.

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

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

On this put up, we’re going to take a better take a look at the Sendable protocol, and the @Sendable annotation for closures. By the top of this put up, you need to have a very good understanding of the issues that Sendable (and @Sendable) purpose to resolve, how they work, and the way you should utilize them in your code.

Understanding the issues solved by Sendable

One of many trickiest elements of a concurrent program is to make sure information consistency. Or in different phrases, thread security. Once we go 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 loads. In apps that don’t actually carry out concurrent work, it’s unlikely that two duties try and entry and / or mutate a chunk of state at the very same time. (However not not possible)

For instance, you could be grabbing information from the community, after which passing the obtained information round to a few features in your important thread.

As a result of nature of the principle thread, you’ll be able to safely assume that your entire code runs sequentially, and no two processes in your utility will likely be engaged on the identical referencea on the similar time, probably creating an information race.

To briefly outline an information race, it’s when two or extra elements of your code try and entry the identical information in reminiscence, and at the least one in all these accesses is a write motion. When this occurs, you’ll be able to by no means make certain in regards to the order through which the reads and writes occur, and you may even run into crashes for dangerous reminiscence accesses. All in all, information races aren’t any enjoyable.

Whereas actors are a incredible strategy to construct objects that appropriately isolate and synchronize entry to their mutable state, they’ll’t remedy all of our information races. And extra importantly, it may not be affordable so that you can rewrite your entire 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 may not look too dangerous. We now have a category that acts as a easy cache for date formatters, and we’ve got a process group that can run a bunch of code in parallel. Every process will seize a random date format from the record of potential format and asks the cache for a date formatter.

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

Nonetheless, as a result of our duties run in parallel there’s an opportunity for information races right here. One fast repair can be to make our FormatterCache an actor and this could remedy our potential information race. Whereas that may be a very good resolution (and truly the perfect resolution should you ask me) the compiler tells us one thing else once we attempt to compile the code above:

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

This warning is attempting to inform us that we’re doing one thing that’s probably harmful. We’re capturing a price that can’t be safely handed by way of concurrency boundaries in a closure that’s imagined to be safely handed by way of concurrency boundaries.

⚠️ If the instance above doesn’t produce a warning for you, you will need to allow strict concurrency checking in your mission’s construct settings for stricter Sendable checks (amongst different concurrency checks). You’ll be able to allow strict concurrecy settings in your goal’s construct settings. Check out this web page should you’re undecided how to do that.

With the ability to be safely handed by way of concurrency boundaries basically signifies that a price could be safely accessed and mutated from a number of duties concurrently with out inflicting information 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 examine whether or not an object is certainly Sendable by assembly the Sendable necessities.

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

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

Sendable and worth sorts

In Swift, worth sorts present loads of thread security out of the field. Whenever you go a price sort from one place to the following, a duplicate is created which signifies that every place that holds a duplicate of your worth sort can freely mutate its copy with out affecting different elements of the code.

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

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

Let’s take a look at an instance:

// This struct isn't sendable
struct Film {
    let formatterCache = FormatterCache()
    let releaseDate = Date()
    var formattedReleaseDate: String {
        let formatter = formatterCache.formatter(for: "YYYY")
        return formatter.string(from: releaseDate)
    }
}

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

I do know that this instance is just a little 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. Nonetheless, FormatterCache is a category that is not Sendable. Since our Film struct doesn’t maintain a duplicate of the FormatterCache however a reference, all copies of Film can be trying on the similar situations of the FormatterCache, which signifies that we could be taking a look at information races if a number of Film copies would try and, for instance, work together with the formatterCache.

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

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

Usually talking, the compiler will infer your structs to be Sendable when wanted. Nonetheless, you’ll be able to manually add Sendable conformance if you would like:

struct Film: Sendable {
    let formatterCache = FormatterCache()
    let releaseDate = Date()
    var formattedReleaseDate: String {
        let formatter = formatterCache.formatter(for: "YYYY")
        return formatter.string(from: releaseDate)
    }
}

Sendable and lessons

Whereas each structs and actors are implicitly Sendable, lessons will not be. That’s as a result of lessons are loads much less protected by their nature; all people that receives an occasion of a category really receives a reference to that occasion. Which means that 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 are able to’t make our lessons Sendable, it simply signifies that we have to add the conformance manually, and manually be sure that our lessons are literally Sendable.

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

last 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. Which means that they have to both be Sendable lessons, worth sorts, or actors. This requirement is equivalent to the necessities for Sendable structs.

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

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

Nonetheless, 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 show it.

In these circumstances, we are able to fall again on unchecked Sendable conformance.

Unchecked Sendable conformance

Whenever you’re working with codebases that predate Swift Concurrency, chances are high that you simply’re slowly working your manner by way of your app with a view to introduce concurrency options. Which means that a few of your objects might want to work in your async code, in addition to in your sync code. Which means that utilizing actor to isolate mutable state in a reference sort may not 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 {
    personal var formatters = [String: DateFormatter]()
    personal 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 excellent (we might be utilizing a barrier or possibly even a plain previous lock as a substitute), it really works. Nonetheless, we are able to’t add Sendable conformance to our class as a result of formatters isn’t Sendable.

To repair this, we are able to 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 the entire necessities.

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

Utilizing @Sendable on closures

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

A number 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. Which means that any state that the closure captures should be Sendable as a result of the closure could be handed throughout concurrency boundaries.

In different phrases, this closure will run in a concurrent method so we need to guarantee that we’re not by chance introducing an information 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 atmosphere.

Tip: to be taught extra about closures in Swift, check out my put up that explains closures in nice element.

Abstract

On this put up, you’ve realized in regards to the Sendable and @Sendable options of Swift Concurrency. You realized why concurrent packages require additional security round mutable state, and state that’s handed throughout concurrency boundaries with a view to keep away from information races.

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

Lastly, you realized 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 put up. If in case you have any questions, suggestions, or ideas to assist me enhance the reference then be happy to succeed in out to me on Twitter.



Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles