Give your simulator superpowers

Give your Xcode
Simulator extra features

Sendable and @Sendable closures explained with code examples

Sendable and @Sendable are part of the concurrency changes that arrived in Swift 5.5 and address a challenging problem of type-checking values passed between structured concurrency constructs and actor messages.

Before diving into the topic of sendables, I encourage you to read up on my articles around async/await, actors, and actor isolation. These articles cover the basics of the new concurrency changes, which directly connect to the techniques explained in this article.

When should I use Sendable?

The Sendable protocol and closure indicate whether the public API of the passed values passed thread-safe to the compiler. A public API is safe to use across concurrency domains when there are no public mutators, an internal locking system is in place, or mutators implement copy-on write like with value types.

Many types of the standard library already support the Sendable protocol, taking away the requirement to add conformance to many types. As a result of the standard library support, the compiler can implicitly create support for your custom types.

For example, integers support the protocol:

extension Int: Sendable {}

Once we create a value type struct with a single property of type int, we implicitly get support for the Sendable protocol:

// Implicitly conforms to Sendable
struct Article {
    var views: Int
}

At the same time, the following class example of the same article would not have implicit conformance:

// Does not implicitly conform to Sendable
class Article {
    var views: Int
}

The class does not conform because it is a reference type and therefore mutable from other concurrent domains. In other words, the class article is not thread-safe to pass around, and the compiler can’t implicitly mark it as Sendable.

Implicit conformance when using generics and enums

It’s good to understand that the compiler does not add implicit conformance to generic types if the generic type does not conform to Sendable.

// No implicit conformance to Sendable because Value does not conform to Sendable
struct Container<Value> {
    var child: Value
}

However, if we add a protocol requirement to our generic value, we will get implicit support:

// Container implicitly conforms to Sendable as all its public properties do so too.
struct Container<Value: Sendable> {
    var child: Value
}

The same counts for enums with associated values:

Implicit Sendable protocol conformance won't work if children do not conform to Sendable.
Implicit Sendable protocol conformance won’t work if children do not conform to Sendable.

You can see that we automatically get an error from the compiler:

Associated value ‘loggedIn(name:)’ of ‘Sendable’-conforming enum ‘State’ has non-sendable type ‘(name: NSAttributedString)’

We can solve the error by using a value type String instead, as it already conforms to Sendable:

enum State: Sendable {
    case loggedOut
    case loggedIn(name: String)
}

Throwing errors from thread-safe instances

The same rules apply to errors that want to conform to Sendable:

struct ArticleSavingError: Error {
    var author: NonFinalAuthor
}

extension ArticleSavingError: Sendable { }

As the author is non-final and not thread-safe (more about that later), we will run into the following error:

Stored property ‘author’ of ‘Sendable’-conforming struct ‘ArticleSavingError’ has non-sendable type ‘NonFinalAuthor’

You can solve the error by making sure all members of ArticleSavingError conform to Sendable.

How to use the Sendable protocol

Implicit conformance takes away a lot of cases in which we need to add conformance to the Sendable protocol ourselves. However, there are cases in which the compiler does not add implicit conformance while we know that our type is thread-safe.

Common examples of types that are not implicitly sendable but can be marked as such are immutable classes and classes with internal locking mechanisms:

/// User is immutable and therefore thread-safe, so can conform to Sendable
final class User: Sendable {
    let name: String

    init(name: String) { self.name = name }
}

You need to mark mutable classes with the @unchecked attribute to indicate our class is thread-safe due to internal locking mechanisms:

extension DispatchQueue {
    static let userMutatingLock = DispatchQueue(label: "person.lock.queue")
}

final class MutableUser: @unchecked Sendable {
    private var name: String = ""

    func updateName(_ name: String) {
        DispatchQueue.userMutatingLock.sync {
            self.name = name
        }
    }
}

The restriction of conforming to Sendable in the same source file

Sendable protocol conformance must happen within the same source file to ensure that the compiler checks all visible members for thread safety.

For example, you could define the following type within a module like a Swift package:

public struct Article {
    internal var title: String
}

The article is public, while the title is internal and not visible outside the module. Therefore, the compiler can’t apply Sendable conformance outside of the source file as it has no visibility of the title property, even though the title is using a Sendable type String.

The same problem occurs when trying to conform an immutable non-final class to Sendable:

Non-final immutable classes can't conform to Sendable
Non-final immutable classes can’t conform to Sendable

Since the class is non-final, we can’t conform to Sendable as we’re unsure whether other classes will inherit from User with non-Sendable members. Therefore, we would run into the following error:

Non-final class ‘User’ cannot conform to `Sendable`; use `@unchecked Sendable`

As you can see, the compiler suggests using @unchecked Sendable. We can add this attribute to our user instance and get rid of the error:

class User: @unchecked Sendable {
    let name: String

    init(name: String) { self.name = name }
}

However, this does require us to ensure it’s always thread-safe whenever we inherit from User. As we add extra responsibility to ourselves and our colleagues, I would discourage using this attribute instead of using composition, final classes, or value types.

How to use @Sendable

Functions can be passed across concurrency domains and will therefore require sendable conformance too. However, functions can’t conform to protocols, so Swift introduced the @Sendable attribute. Examples of functions that you can pass around are global function declarations, closures, and accessors like getters and setters.

Part of the motivation of SE-302 is performing as little synchronization as possible:

we want the vast majority of code in such a system to be synchronization free

Using the @Sendable attribute, we will tell the compiler that he doesn’t need extra synchronization as all captured values in the closure are thread-safe. A typical example would be using closures from within Actor isolation:

actor ArticlesList {
    func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
        // ...
    }
}

In case you would use the closure with a non-sendable type, we would run into an error:

let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in
 
    // Error: Reference to captured var 'searchKeyword' in concurrently-executing code
    guard let searchKeyword = searchKeyword else { return false }
    return article.title == searchKeyword.string
}

Of course, we can quickly solve this case by using a regular String instead, but it demonstrates how the compiler helps us to enforce thread safety.

Preparing your code for Swift 6 with strict concurrency checking

Xcode 14 allows you to enable strict concurrency checking through the SWIFT_STRICT_CONCURRENCY build setting:

Enable strict concurrency checking to fix sendable conformances and prepare your code for Swift 6.
Enable strict concurrency checking to fix sendable conformances and prepare your code for Swift 6.

This build setting controls the compiler enforcement level of Sendable and actor-isolation checking.

  • Minimal: The compiler will only diagnose instances explicitly marked with Sendable conformance and equals the behavior of Swift 5.5 and 5.6. There won’t be any warnings or errors.
  • Targeted: Enforces Sendable constraints and performs actor-isolation checking for all your code that adopted concurrency like async/await. The compiler will also check Instances that explicitly adopt Sendable. This mode tries to strike a balance between compatibility with existing code and catching potential data races.
  • Complete: Matches the intended Swift 6 semantics to check and eliminate data races. This mode checks everything the other two modes do as well but performs these checks for all code in your project.

The strict concurrency checking build setting helps Swift move forward to data-race safety. Each of the warnings triggered related to this build setting might indicate a potential data race in your code. Therefore, it’s essential to consider enabling strict concurrency checking to validate your code.

Enabling strict concurrency in Xcode 14

The number of warnings you’ll get depends on how often you’ve used concurrency in your project. For Stock Analyzer, I had about 17 warnings to solve:

Concurrency related warnings indicating potential data races.
Concurrency-related warnings indicating potential data races.

These warnings can be intimidating, but with the knowledge from this article, you should be able to get rid of most of them and prevent data races from taking place. However, some warnings are out of your control since an external module triggers them. In my case, I’ve had a warning related to SWHighlight that does not conform to Sendable, while Apple defined it in their SharedWithYou framework.

The compiler triggered several warnings related to this same issue:

  • Capture of ‘highlight’ with non-sendable type ‘SWHighlight?’ in a @Sendable closure
  • Stored property ‘highlight’ of ‘Sendable’-conforming struct ‘SharedSymbol’ has non-sendable type ‘SWHighlight?’

One way of solving this would be to add Sendable conformance ourselves:

extension SWHighlight: Sendable { }

However, you’ll run into the following error:

Conformance to ‘Sendable’ must occur in the same source file as class ‘SWHighlight’; use ‘@unchecked Sendable’ for retroactive conformance

Following the suggestion, you would change the code as follows:

extension SWHighlight: @unchecked Sendable { }

The @unchecked attribute tells the compiler to disable concurrency checking for SWHighlight instances. While this removes the warnings we’ve seen earlier, it doesn’t solve the potential race conditions. It would be best if you only used the @unchecked attribute for reference types that do their internal synchronization without actors. These types are thread-safe, but there’s no way for the compiler to verify them. You can mark them as unchecked, in which you tell the compiler to take care of data races yourself.

In the above example of the SharedWithYou framework, it’s better to wait for the library owners to add Sendable support. In this case, it would mean waiting for Apple to indicate Sendable conformance for SWHighlight instances. For those libraries, you can temporarily disable Sendable warnings by making use of the @preconcurrency attribute:

@preconcurrency import SharedWithYou

It’s important to understand that we didn’t solve the warnings but just disabled them. There’s still a possibility of data races occurring with code from these libraries. If you’re using instances from these frameworks, you need to consider whether instances are actually thread-safe. Once your used framework gets updated with Sendable conformance, you can remove the @preconcurrency attribute and fix potentially triggered warnings.

Continuing your journey into Swift Concurrency

The concurrency changes are more than just async-await and include many new features you can benefit from in your code. Now that you’ve learned about Sendable, it’s time to dive into other concurrency features:

Conclusion

The Sendable protocol and @Sendable attribute for functions make it possible to tell the compiler about thread safety when working with concurrency in Swift. Swift introduced both features to reach the bigger goal of the Swift Concurrency effort, which is providing a mechanism to isolate states in concurrent programs to eliminate data races. The compiler will help us in many cases with implicit conformance to Sendable, but we can always add conformance ourselves.

If you like to learn more tips on Swift, check out the Swift category page. Feel free to contact me or tweet me on Twitter if you have any additional suggestions or feedback.

Thanks!

 

Featured SwiftLee Jobs

Loading RSS Feed

Browse more Swift related Jobs, or add your own on SwiftLee Jobs