© Andrés Ibañez Kautsch 2023
A. I. KautschModern Concurrency on Apple Platformshttps://doi.org/10.1007/978-1-4842-8695-1_7

7. Sendable Types

Andrés Ibañez Kautsch1  
(1)
La Paz, Bolivia
 

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.

Out of the box, Swift has some sendable types that are safe to use concurrently, including:
  1. 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. 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. 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. 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
Most of the time, structs will be sendable out of the box. But there are situations in which they can’t be. Imagine that we have a struct that has a property with a class type, like in Listing 7-1.
class TVShow {
    let title: String
    var rating: Int
    init(title: String, rating: Int) {
        self.title = title
        self.rating = rating
    }
}
struct TVShowLibrary {
    var shows: [TVShow] = []
}
Listing 7-1

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.

If you try to cross isolation boundaries (declaring a TVShowLibrary in a nonisolated context and capturing it within a Task.detached {}, for example) with this struct, the compiler will stop you, even though it is a struct. Normal Task {} captures will not be affected, because they will inherit the actor from the top context, which in this case is the @MainActor. In Listing 7-2, we declare a TVShowLibrary instance and then we capture it within two Tasks.
var tvShowLibrary = TVShowLibrary()
let show = TVShow(name: "Card Captor Sakura")
tvShowLibrary.shows += [show]
Task {
    tvShowLibrary.shows.first?.rating = 20
    print("Current score Task 1: (tvShowLibrary.shows.first!.rating)")
}
Task {
    tvShowLibrary.shows.first?.rating = 30
    print("Current score Task 2: (tvShowLibrary.shows.first!.rating)")
}
Listing 7-2

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.

Structs will be sendable by default, unless they have properties that are not sendable as well. Our TVShowLibrary object is not sendable, because it has an array of TVShows. TVShow is a class, which is not sendable by default. Because TVShow is not sendable, our entire TVShowLibrary is not sendable either. We can observe this behavior better if we change the Task {} calls from Listing 7-2 into detached tasks. Listing 7-3 makes this change.
var tvShowLibrary = TVShowLibrary()
let show = TVShow(name: "Card Captor Sakura")
tvShowLibrary.shows += [show]
Task.detached {
    tvShowLibrary.shows.first?.rating = 20
    print("Current score Task 1: (tvShowLibrary.shows.first!.rating)")
}
Task.detached {
    tvShowLibrary.shows.first?.rating = 30
    print("Current score Task 2: (tvShowLibrary.shows.first!.rating)")
}
Listing 7-3

Capturing mutable non-sendable types from detached tasks

Now this is more interesting. Because Task.detached does not inherit the actor from the top context, each of them run completely independently. That would certainly cause data races, and Swift is there to protect you from it. Figure 7-1 shows the errors you would get.

A screenshot represents the TVShow code, which consists of the variables tv show library and Task. detached declared twice with Tasks 1 and 2 highlighted to indicate the reference.

Figure 7-1

The compiler protecting you from data races

Note

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
Classes are not sendable by default. You can, however, conform to the Sendable protocol when they are marked as final and have read-only properties. You can mark the TVShow as conforming to Sendable, and your code would compile, but not without getting any warnings. Figure 7-2 shows the warnings we get when attempting to do so.

A screenshot represents the code of the class TV Show, which consists of seven lines of code and the first and second lines with some errors highlighted.

Figure 7-2

Conforming a class to sendable

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.

Ultimately, the only way we could fix the class is by opting out of compiler safety checks. To do this, add the @unchecked attribute before the Sendable conformance. Listing 7-4 shows the new TVShow class that does not yield any warnings.
class TVShow: @unchecked Sendable {
    let name: String
    var rating: Int = 0
    init(name: String) {
        self.name = name
    }
}
Listing 7-4

Opting out from compile-time checks

This needs to be said again. You are opting out from compiler checks when you use @unchecked. Because this class has no synchronization of its own, it will cause data races when multiple tasks or actors mutate it at the same time. You can add your own synchronization mechanism by using something like NSLock. Listing 7-5 shows a very simple implementation of this .
class TVShow: @unchecked Sendable {
    let name: String
    var rating: Int = 0 {
        willSet {
            mutex.lock()
        }
        didSet {
            mutex.unlock()
        }
    }
    let mutex = NSLock()
    init(name: String) {
        self.name = name
    }
}
Listing 7-5

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.

Caution

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
In Swift, closures are first-class citizens. You can pass closures and function references around other parts of your code to call them anytime. You have likely seen this when working with the filter, map, and reduce methods of collections in Swift. To recap, Listing 7-6 shows a simple use of the filter method.
library.shows.filter { $0.rating >= 10 && $0.rating <= 30 }
Listing 7-6

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.

If you ever look at the documentation for Task, you will see that its signature is the same one in Listing 7-7.
@frozen struct Task<Success, Failure> where Success : Sendable, Failure : Error
Listing 7-7

Declaration for Task

It has a generic type Success that conforms to Sendable. So tasks operate on Sendable types. Now let’s take a look at the declarations for its initializer and for the detached static method. They are both in Listing 7-8.
@discardableResult init(
    priority: TaskPriority? = nil,
    operation: @escaping () async -> Success
)
//...
@discardableResult static func detached(
    priority: TaskPriority? = nil,
    operation: @escaping () async throws -> Success
) -> Task<Success, Failure>
Listing 7-8

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.

We can create our own Sendable functions with this same behavior. Listing 7-9 declares a method with a @Sendable closure . This method will print something before executing the method, and something after doing so. You could use a function like this to benchmark your methods.
func logBeginningAndEnd(operation: @Sendable () -> Void) {
    print("Calling Closure")
    operation()
    print("Closure finished")
}
Listing 7-9

Declaring a method that takes a @Sendable closure

Now let’s try calling this method, passing something that is not sendable. Listing 7-10 creates a new TVShowLibrary instance and makes use of this method.
var tvShowLibrary = TVShowLibrary()
logBeginningAndEnd {
    let showsCount = tvShowLibrary.shows.count
    print("(showsCount) Shows")
}
Listing 7-10

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.

You know that in Swift, you can pass an actual function to a closure parameter. Listing 7-11 attempts to pass a function to a method that expects a closure, but the function we are passing is not @Sendable .
func doSomething() {
}
logBeginningAndEnd(operation: doSomething)
Listing 7-11

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”.

So, if you are certain that your functions can be shared across different threads, don’t forget to mark them as @Sendable so the system doesn’t send out any unnecessary warnings, like in Listing 7-12.
@Sendable func doSomething() {
}
Listing 7-12

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.

Finally, if you have a variable that can store a closure, you can constrain it to @Sendable closures as well. Listing 7-13 declares a class which has a property that can store a @Sendable closure .
class MyClass {
    var aClosure: @Sendable () -> Void
    init(aClosure: @escaping @Sendable () -> Void) {
        self.aClosure = aClosure
    }
}
Listing 7-13

Declaring a variable that can store a @Sendable closure

Finally, when you pass in a closure to a method, and you want that closure to be @Sendable, you can use the @Sendable in syntax at the top of your closure to mark it as such. Listing 7-14 passes an explicit @Sendable closure to the MyClass initializer.
let myObject = MyClass { @Sendable in
    let a = 3
}
Listing 7-14

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.226.24