The simplest concurrent programs are fairly isolated, even when they are dealing with concurrency. But in complex programs, you will need to pass data around across isolation boundaries, from one async task to another, from one actor to another. The data will need to be thread-safe. As we learned with Actors, sharing mutable data across asynchronous domains can result in data races and therefore data corruption.
Swift needed a way to define a safe way to share data across isolation boundaries. The Swift Evolution folks came up with a very elegant solution and introduced sendable types.
Understanding Sendable Types
A sendable type can be shared across asynchronous tasks and across actors. By definition, a sendable type needs to be protected against data races. This type should be impossible to mutate from more than one operation at the same time. These types should offer synchronized access only.
- 1.
Value types. Value types such as enums and structs are sendable as long as they don’t have any properties that aren’t sendable. As we discussed when we talked about actors, value types are immutable. If you attempt to mutate a value type, a new copy is created with the new data. This makes structs and enums work out of the box with the new concurrency system. Multiple copies may be created, but an existing copy will always exist as-is until it is deleted.
- 2.
Actors. Actors were made to share mutable data, so their whole purpose of existing was to be used in the new concurrency model. You can share actors across isolation boundaries without any constraints, keeping in mind that they may have nonisolated functionality. Actors are always sendable.
- 3.
Classes. These can be made sendable, but they will need to have certain constraints. Recall that classes are reference types and any modifications to an instance of a class is a modification to the real data. No copy is created unlike structs. A class marked as final that has read-only properties can very easily be made sendable. Another way to make a class sendable, if it has mutable properties and is not marked private, is by implementing your own synchronization mechanism within the class. This sounds hard to do – and it is.
- 4.
Methods and closures can be made sendable, but explicitly.
In many cases, Swift can infer the sendability of types. Structs and actors are one example. But if you do the work to make a class thread-safe, Swift has no way to do that, and it will stop you from using it in a way the compiler deems dangerous.
The Sendable Protocol
Sendable types in Swift outside of value types and actors conform to the Sendable protocol. By simply conforming to this protocol, the compiler will check your usage of sendable types to ensure you are not trying to use them in a way that may result in data races.
Analyzing Sendable Types
To better understand how sendable types behave, we will analyze them to learn about their quirks and special considerations.
Structs
A struct with a property which is an array of classes
This code declares a struct, TVShowLibrary , whose only property is an array of TVShows. TVShow, however, is a mutable class. It makes sense that we will want to change the rating of a TV show a few months after watching it.
Capturing a struct with non-sendable types in Tasks
Intuitively, you may think that the compiler should stop you from doing this. After all, you are capturing a mutable value and mutating it from two tasks at the same time! But this is perfectly acceptable because both Task calls have the same actor of whatever launched them. If you launched the tasks from a non-asynchronous context, they would inherit the main actor.
And the output will always be what you expect it to be. The task above will always print the rating is 20, and the task below will always print the rating is 30. No race conditions will take place.
Capturing mutable non-sendable types from detached tasks
There is a difference to how Xcode 13 and Xcode 14 behave in this scenario. In Xcode 13, you will get the error shown in Figure 7-1. In Xcode 14, you will get the same messages, but as warnings.
The most sensible solution, in this case, is changing the TVShowLibrary so it is an actor instead of a struct. This is the perfect usage for an actor because multiple tasks are interested in mutating it.
Classes
Marking the class as final will silence the top warning, but not the bottom one. Making the rating a let variable will silence the bottom warning, but we will not be able to mutate the score later. As a rule of thumb, if you have classes that need to be sendable, marking them as final and making everything read-only is the way to go, but it is not possible, as it is the case here.
Opting out from compile-time checks
Manually implementing a lock to the synchronize state
By using the lock, we can lock it before we set the variable, and unlock It after it is set. This example is simple enough, but more complex classes won’t have the same benefit.
I do not recommend you use this code in production unless you truly understand how the concurrency primitives such as locks work. The new concurrency system aims to aid developers into not concerning themselves with such details.
Closures
Using the filter method
Because you can pass functions and closures around other pieces of your code, you can be more than certain that they should be able to be sendable as well.
Neither functions or closures can conform to protocols, but we can use the @Sendable attribute instead. When we use this attribute, the compiler will understand that it doesn’t need to synchronize for anything, because the body of @Sendable functions are sendable, and sendable closures can only capture sendable types.
Declaration for Task
Declarations for a Task’s initializer and the detached static method
We have established that Success is something that conforms to Sendable. This is how Swift does its magic to ensure that non-sendable types cannot be transferred to other concurrent domains. The initializer takes a closure that returns a Sendable type, and the detached method returns a Task with the same requirements.
Declaring a method that takes a @Sendable closure
Calling a @Sendable closure capturing a non-sendable type
The compiler will not let you compile this, and you will see the same errors as in Figure 7-1.
Passing a non-sendable function to a sendable closure
Swift will protect you here as well, but with a warning. Swift will warn you that “Converting non-sendable function value to ‘@Sendable () -> Void’ may introduce data races”.
Marking the doSomething function as Sendable will silence the warning
And naturally, don’t go on marking every single function or method you have as @Sendable. Only do this when you are certain the functions are safe to use in different concurrency domains and you intend to use them as such.
Declaring a variable that can store a @Sendable closure
Using the @Sendable in syntax
In this specific example, it’s not necessary to do this, because MyClass is already treating the closure as @Sendable, but there will be times in which this will be useful, like when we talk about async sequences.
Summary
In this chapter, you learned about the mechanism that Swift uses to ensure that data cannot be mutated across different concurrency domains, or different threads. The Sendable protocol for types and the @Sendable attribute helps us tell the compiler that sharing data across concurrent domains should be allowed because they are protected against data races. Many types will have the conformance automatically by default, such us read-only structs and actor types. Swift will check for Sendable conformance when attempting to share these types across different threads. If we have a type that can be sendable but the Swift compiler cannot infer it, we can mark it as @unchecked @Sendable to tell Swift that this is indeed a sendable type, but we are responsible for synchronizing its internal state.