Swift Sendable protocol and @Sendable attribute

Mar 11, 2023#swift#concurrency

Swift actors and structured concurrency tasks provide a mechanism for isolating state in concurrent programs to eliminate data races. When and how do we allow data to be transferred between concurrency domains? Such transfers occur in arguments and results of actor method calls, or tasks created by structured concurrency.

Let’s take a look at some common cases that are safe to transfer:

  • Simple values like integers or strings.
  • Structs, enums and tuples with data they contain is itself safe to transfer.
  • Functions, closures with an empty capture list.
  • Immutable classes with the state within it never mutates.
  • Internally synchronized classes as they protect their state with explicit synchronization (mutexes, atomics, etc).
  • All actors since the mutable state within an actor implicitly protected by the actor mailbox.

However, everything isn’t simple here. What about types contain general class references, closures that capture mutable state, and other non-value types. We need a way to differentiate between the cases that are safe to transfer and those that are not.

Sendable “marker” protocol

Swift introduces Sendable as a marker protocol, which indicates that the protocol has some semantic property but is entirely a compile-time notion that does not have any impact at runtime. Marker protocols have the following restrictions:

  • They cannot have requirements of any kind.
  • They cannot inherit from non-marker protocols.
  • They cannot be used in is or as? checks.
  • They cannot be used in a generic constraint for a conditional protocol conformance to a non-marker protocol.

The Sendable protocol models types that are allowed to be safely passed across concurrency domains by copying the value. This includes value-semantic types, references to immutable reference types, internally synchronized reference types, @Sendable closures, and potentially other future type system extensions for unique ownership etc.

Sendable conformance

Unless a type implicitly conforms to Sendable in some cases, you need to declare conformance to Sendable explicitly in the same file as the type’s declaration.

  1. Sendable tuples must have all elements conform to Sendable.
  2. Metatypes always conform to Sendable because they are immutable.
  3. Sendable structures and enumerations must have only sendable members or associated values. Implicitly conform to Sendable if frozen, or aren’t public and aren’t marked @usableFromInline.
  4. All actors implicitly conform to Sendable because actors ensure that all access to their mutable state is performed sequentially.
  5. Sendable classes must be (1) marked final, (2) contain only stored properties that are immutable and sendable, and (3) have no superclass or have NSObject as the superclass.
final class Movie: Sendable {
  let formattedReleaseDate = "2022"
}
  1. Sendable functions and closures must use only by-value captures, and the captured values must be of a sendable type. In a context that expects a sendable closure, a closure that satisfies the requirements implicitly conforms to Sendable. Otherwise, you mark sendable functions and closures with the @Sendable attribute explicitly.
let hello = { @Sendable (name: String) -> String in
  return "Hello" + name
}

func runLater(_ function: @escaping @Sendable () -> Void) {
  DispatchQueue.global().asyncAfter(deadline: .now() + 3, execute: function)
}

func globalFunction(arr: [Int]) {
  var state = 42

  @Sendable
  func mutateLocalState2(value: Int) {
    // Error: 'state' is captured as a let because of @Sendable
    state += value
  }
}
  1. Sendable generics must have instance data is guaranteed to be of Sendable type.
// Implicitly conforms to Sendable
struct Foo<T: Sendable> {  
  var value: T
}

// Not implicitly conform to Sendable because T does not conform to Sendable
struct Foo<T> {
  var value: T
}

@unchecked Sendable

To declare conformance to Sendable without any compiler enforcement, write @unchecked Sendable. You are responsible for the correctness of unchecked sendable types, for example, by protecting all access to its state with a lock or a queue. Unchecked conformance to Sendable also disables enforcement of the rule that conformance must be in the same file.

class Foo: @unchecked Sendable {
  // implementation unchanged
}