This article, the third in a series on Swift’s New Concurrency Framework, focuses on Sendable introduced in Swift 5.6.

This series of articles takes a step-by-step look at what is involved in Swift’s new concurrency framework, as follows:

  • Swift new concurrency framework async/await

  • Actor for Swift’s new concurrency framework

  • Sendable for Swift’s new concurrency framework

  • Task for Swift’s new concurrency framework

This post is also published on my personal blog

Overview


The book picks up where it left off (” Actors for Swift’s new Concurrency Framework “), and this article focuses on what Sendable is and how to solve the problems mentioned above.

/// The Sendable protocol indicates that value of the given type can
/// be safely used in concurrent code.
public protocol Sendable {}
Copy the code

Sendable is an empty protocol:

The types used to announce to the outside world that implement the protocol are safe to use in a concurrent environment and, more accurately, can be passed freely across actors.

This is a “semantic” requirement.

A protocol like Sendable has a proper name: “Marker Protocols”, which has the following characteristics:

  • Has specific semantic properties, and they are compile-time properties rather than runtime properties.

    For example, the semantic attribute of Sendable requires that it can be safely passed across actors in a concurrent manner.

  • The protocol body must be empty;

  • Cannot inherit from non-marker protocols (which is actually an extension of point 2);

  • Cannot be used as a type name for is, AS? Operations such as

    Marker protocol ‘Sendable’ cannot be used in a conditional cast.

  • Cannot be used as a constraint on generic types to make a type adhere to a non-marker protocol, as in:

     protocol P {
       func test(a)
     }
    
     class A<T> {}
    
     // Error: Conditional conformance to non-marker protocol 'P' cannot depend on conformance of 'T' to non-marker protocol 'Sendable'
     extension A: P where T: Sendable {
       func test(a){}}Copy the code

We know that Value semantics perform copying when they are passed (as function arguments, return values, etc.), meaning that they are safe to pass across actors. Therefore, these types implicitly automatically comply with the Sendable protocol, as in:

  • Basic types, Int, String, Bool, etc.

  • Struct without reference type members;

  • Enum with no reference type associated value;

  • A collection of elements whose types comply with Sendable, such as Array and Dictionary.

Of course, all actor types also automatically comply with the Sendable protocol.

In fact, all actors comply with the Actor protocol, which inherits from Sendable:

@available(macOS 10.15.iOS 13.0.watchOS 6.0.tvOS 13.0.*)
public protocol Actor : AnyObject.Sendable {
    nonisolated var unownedExecutor: UnownedSerialExecutor { get}}Copy the code

The class needs to proactively declare compliance with the Sendable protocol, with the following restrictions:

  • Warning: Non-final class ‘X’ cannot conform to ‘Sendable’; use ‘ @unchecked Sendable’

  • Note The class’s Stored property must be immutable, otherwise Warning: Stored property ‘x’ of ‘Sendable’ -corner-class ‘x’ is mutable

  • All storage attributes of class must comply with the Sendable protocol, otherwise Warning: Stored property ‘y’ of ‘Sendable’-conforming class ‘X’ has non-sendable type ‘Y’

  • The ancestor of class (if any) must obey the Sendable protocol or NSObject, otherwise Error: ‘Sendable’ class ‘X’ cannot inherit from another class other than ‘NSObject’.

These restrictions are well understood and are necessary to ensure class data security with the Sendable protocol implemented.

Back to the above example:

extension AccountManager {
  func user(a) async -> User {
    // Warning: Non-sendable type 'User' returned by implicitly asynchronous call to actor-isolated instance method 'user()' cannot cross actor boundary
    return await bankAccount.user()
  }
}
Copy the code

Obviously, to eliminate Warning in the example, you can simply have User implement the Sendable protocol.

For this example, User has two transformation options:

  • Struct (struct);

    struct User {
      var name: String
      var age: Int
    }
    Copy the code
  • Implement Sendable protocol manually:

    final
    class User: Sendable {
      let name: String
      let age: Int
    }
    Copy the code

On reflection, isn’t Sendable too final, immutable property for the class that implements it? !

A little too idealistic, a little unrealistic

From the point of view of concurrency security, it can be guaranteed by the traditional serial queue, lock and other mechanisms.

In this case, you can tell the compiler to unchecked Sendable semantics using the @unchecked attribute, as shown in the following example:

// It is equivalent to saying that the concurrency security of the User is guaranteed by the developer, without the compiler checking
class User: @unchecked Sendable {
  var name: String
  var age: Int
}
Copy the code

Sendable as a protocol can only be used for general types, not functions, closures, and so on.

That’s where @sendable comes in.

@Sendable


Functions and closures decorated with @sendable can be passed across actors.

For the example mentioned above:

extension BankAccount {
  func addAge(amount: Int.completion: (Int) - >Void) {
    age + = amount
    completion(age)
  }
}

extension AccountManager {
  func addAge(a) async {
    // Wraning: Non-sendable type '(Int) -> Void' passed in implicitly asynchronous call to actor-isolated instance method 'addAge(amount:completion:)' cannot cross actor boundary
    await bankAccount.addAge(amount: 1, completion: { age in
      print(age)
    })
  }
}
Copy the code

Simply add @sendable to the completion parameter of the addAge method:

func addAge(amount: Int.completion: @Sendable (User) - >Void)
Copy the code

To sum up, what does it really mean to decorate Closure with @sendable?

The Closure implementer tells the Closure implementer that this Closure may be used in a concurrent environment, please be careful about data security.

Therefore, if the externally provided interface involves a Closure (as a method parameter, return value) that may be executed in a concurrent environment, apply@SendableModification.

Following this principle, external actor methods that involve Closure also apply the @sendable modifier.

extension Task where Failure= =Error {
  public init(priority: TaskPriority? = nil.operation: @escaping @Sendable(a)async throws -> Success)
}
Copy the code

For example, the operation closure of a Task is executed in a concurrent environment, so the @sendable modifier is used.

Of course the compiler does various compliance checks on @Sendable Closure’s implementation:

  • Error: Actor-Isolated Property ‘x’ can not be referenced from a Sendable closure; (The reason is also simple, @sendable Closure may execute in a concurrent environment, which conflicts with actor serial protection of data)

    This restriction is not applied if the @sendable closure is asynchronous (@sendable () async).

    You can think about why?

  • Error: Mutation of captured var ‘x’ in Partition-Executing code

  • Warning: Capture of ‘x’ with non-sendable type ‘x’ in a @Sendable Closure

Remember the last crash in actor of Swift’s new concurrency framework:

extension User {
  func testUser(callback: @escaping() - >Void) {
    for _ in 0..<1000 {
      DispatchQueue.global().async {
        callback()
      }
    }
  }
}

extension BankAccount {
  func test(a) {
    let user = User.init(name: "Tom", age: 18)
    user.testUser {
      let b = self.balances[1] ?? 0.0
      self.balances[1] = b + 1
      print("i = \ [0).\(Thread.current), balance = \(String(describing: self.balances[1]))")}}}Copy the code

How to fix the crash?

Think about it

extension User {
  // Callback will execute in a concurrent environment, so it is decorated with '@sendable'
  // In general, @Sendable closure is asynchronous, otherwise the @sendable rule cannot capture actor-Isolated Property
  func test(callback: @escaping @Sendable(a)async -> Void) {
    for _ in 0..<1000 {
      DispatchQueue.global().async {
        // In a synchronous context, an asynchronous context is typically opened by Task
        Task{
          await callback()
        }
      }
    }
  }
}

extension BankAccount {
  func changeBalances(newValue: Double) {
    balances[1] = newValue
  }

  func test(a) {
    let user = User.init(name: "Tom", age: 18)

    user.test { [weak self] in
      guard let self = self else { return }

      let b = await self.balances[1] ?? 0.0
      // Changes to actor-Isolated Property need to be extracted into a separate method
      // Cannot be modified directly in the @sendable closure
      await self.changeBalances(newValue: b + 1)
      print("i = \ [0).\(Thread.current), balance = \(String(describing: await self.balances[1]))")}}}Copy the code

Future Improvement


Apple writes in Protect Mutable State with Swift Actors – WWDC21 that the Swift compiler will prohibit sharing (passing) instances of non-sendable types in the future.

Then, any Warning mentioned in this article will become an Error!

Well, that’s all for Sendable!

summary

  • Sendable itself is a Marker Protocol for compile-time compliance checks;

  • All value semantic types automatically comply with the Sendable protocol;

  • All types that comply with the Sendable protocol can be passed across actors;

  • Sendable modifies methods and closures;

  • Apply the @sendable modifier to closures that will execute in a concurrent environment.

The resources

Swift evolution/0296-async-await. Md at main · Apple /swift evolution · GitHub

Swift evolution/0302-concurrent-value-and-concurrent-closures. Md at Main · Apple/Swift Evolution · GitHub

Swift evolution/0337-support-incremental-migration-to-concurrency-checking. Md at main · Apple/Swift Evolution · GitHub

Swift-evolution /0304- Structured – Concurrency. Md at Main · Apple/Swift-Evolution · GitHub

Swift evolution/0306- Actors. Md at main · Apple/Swift Evolution · GitHub

Swift evolution/0337-support-incremental-migration-to-concurrency-checking. Md at main · Apple/Swift Evolution · GitHub

Understanding async/await in Swift • Andy Ibanez

Concurrency — The Swift Programming Language (Swift 5.6)

Connecting async/await to other Swift code | Swift by Sundell