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:
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:
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:
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:
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:
- MainActor usage in Swift explained to dispatch to the main thread
- How to Use URLSession with Async/Await for Network Requests in Swift
- Async await in Swift explained with code examples
- Swift 6: Incrementally migrate your Xcode projects and packages
- Concurrency-safe global variables to prevent data races
- Unit testing async/await Swift code
- Thread dispatching and Actors: understanding execution
- @preconcurrency: Incremental migration to concurrency checking
- Detached Tasks in Swift explained with code examples
- Task Groups in Swift explained with code examples
- Sendable and @Sendable closures explained with code examples
- AsyncSequence explained with Code Examples
- AsyncThrowingStream and AsyncStream explained with code examples
- Tasks in Swift explained with code examples
- Nonisolated and isolated keywords: Understanding Actor isolation
- Async let explained: call async functions in parallel
- Actors in Swift: how to use and prevent data races
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!