Sendable and @Sendable closures in Swift


Greetings, traveler!

Swift has data types that can be safely transferred from one thread to another. SE-0302 introduces a straightforward way to separate the wheat from the chaff — the sendable data types marked with the Sendable protocol.

Sendable

Basic types

Many data types from the standard library are Sendable by default.

  • “Basic” data types (Int, String, Bool, and others).
  • Optionals, where the wrapped data is a value type.
  • Metatypes, such as Int.self.
  • Collections that contain value types, such as Array<Int> or Dictionary<Int, String>.
  • Tuples where the elements are all value types.

Custom types

  • Since actors automatically handle concurrent access to their state, they are Sendable by default.
  • Structures can be Sendable, but only if their properties have Sendable types.
  • Enumerations can conform to the Sendable protocol if their cases have associated values that conform to the Sendable protocol. Or don’t have any associated values at all.
  • Classes can conform to the Sendable protocol if marked with the ‘final‘ keyword, don’t have parents, or are inherited from the NSObject. The other condition is that these classes must contain only constant properties.

Examples

Let’s examine some examples. First, consider an example with a structure conforming to the Sendable protocol.

struct MyStruct: Sendable {
    let property: Int
}

struct MyStruct: Sendable {
    var property: Int
}

This is an enum example. This one will conform to the Sendable protocol without any problems.

enum NumberKind: Sendable {
    case integer(Int)
    case double(Double)
}

And this one won’t.

enum StringKind: Sendable {
    case regular(String)
    case attributed(NSAttributedString) // ❌ Associated value 'attributed' of 'Sendable'-conforming enum 'StringKind' has non-sendable type 'NSAttributedString'
}

We can handle this situation, but first, let’s look at a class example. Here is the proper class that conforms to the Sendable protocol.

final class User: Sendable {

    private let name: String
    private let password: String

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

But what will the compiler say if we make one of the properties mutable? We will face some problems, of course.

final class User: Sendable {

    private let name: String
    private var password: String // ❌ Stored property 'password' of 'Sendable'-conforming class 'User' is mutable

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

There is a way to ‘fix’ this situation. We can use the @unchecked keyword.

final class User: @unchecked Sendable {
    
    private let queue = DispatchQueue(label: "com.user.queue")    
    private let name: String
    private var password: String

    init(name: String, password: String) {
        self.name = name
        self.password = password
    }
    
    func changePassword(_ newValue: String) {
        queue.sync {
            password = newValue
        }
    }
    
}

Actually, this is not a full-fledged fix. We are just saying that we are responsible for all the consequences.

@Sendable closures

If any methods you have written provide multithreaded access to data, they can be marked with ‘@Sendable‘. In this case, the compiler will check the data types passed to it for security. The same applies to closures.

Example

Consider this example. We have a function that prints a String value using Swift Concurrency.

func displayValue(_ value: String) async {
    Task {
        print(value)
    }
}

In Swift programming language, the default function parameter behavior passes them as constants. This means that the values passed into the function cannot be modified within the function itself.

Since our ‘value’ is a constant, there are no problems with printing it in the Task. However, if you need to modify the values passed in, you can use the ‘inout’ keyword to mark the parameters as modifiable, which allows the functions to change the values and have those changes reflected in the original parameters outside the function.

Let’s mark our method’s parameter with this keyword. We will immediately get an error. The Task won’t let us make our value mutable in concurrently-executing code.

func displayValue(_ value: inout String) async {
    Task {
        print(value) // ❌ Mutable capture of 'inout' parameter 'value' is not allowed in concurrently-executing code
    }
}

But what if we don’t wrap anything into the Task? Sure, the compiler won’t warn us about potential problems. To enforce similar rules around captured values, we can mark our functions or closures with the ‘@Sendable’ keyword. Consider this example.

func performWithDelay(
    _ delay: TimeInterval,
    _ action: @escaping @Sendable () -> Void
) {
    
    DispatchQueue.main.asyncAfter(
        deadline: .now() + delay,
        execute: action
    )
}

func displayValue(_ value: inout String) {
    performWithDelay(3) {
        print(value) // ❌ Mutable capture of 'inout' parameter 'value' is not allowed in concurrently-executing code
    }
}

The compiler will warn us about capturing a variable in a closure that could be executed asynchronously, as this could lead to conflicts. Nice!

Conclusion

We have considered a convenient tool that will make our code safer and save us from problems we can create for ourselves. This concludes this article, and in the following one, we will talk about AsyncStream and AsyncThrowingStream. See you there.