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 by 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 a 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 }
}

Mutable classes need to be marked with the @unchecked attribute to indicate our class is actually 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 itself 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 which is why the @Sendable attribute is introduced. 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

By using the @Sendable attribute we will tell the compiler that no extra synchronization is needed as all captured values in the closure are thread-safe to work with. 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 just using a regular String instead, but it does demonstrate how the compiler helps us to enforce thread safety.

Continuing your journey into Swift Concurrency

The concurrency changes are more than just async-await and include many new features that 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. Both features were introduced 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!