本文是 『 Swift 新并发结构 』系列文章的第二篇,首要介绍 Swift 5.5 引入的 actor。

本系列文章对 Swift 新并发结构中触及的内容逐个进行介绍,内容如下:

  • Swift 新并发结构之 async/await

  • Swift 新并发结构之 actor

  • Swift 新并发结构之 Sendable

  • Swift 新并发结构之 Task

本文同时宣布于我的个人博客

Overview


Swift 新并发模型不仅要处理咱们在『 Swift 新并发结构之 async/await 』一文中说到的异步编程问题,它还致力于处理并发编程中最让人头疼的 Data races 问题。

为此,Swift 引入了 Actor model :

  • Actor 代表一组在并发环境下能够安全拜访的(可变)状况;

  • Actor 经过所谓数据隔离 (Actor isolation) 的办法保证数据安全,其完成原理是 Actor 内部维护了一个串行队列 (mailbox),一切触及数据安全的外部调用都要入队,即它们都是串行履行的。

    Swift 新并发框架之 actor

为此,Swift 引入了 actor 要害字,用于声明 Actor 类型,如:

actor BankAccount {
  let accountNumber: Int
  var balance: Double
  enum BankAccountError: Error {
    case insufficientBalance(Double)
    case authorizeFailed
  }
  init(accountNumber: Int, initialDeposit: Double) {
    self.accountNumber = accountNumber
    self.balance = initialDeposit
  }
  func deposit(amount: Double) {
    assert(amount >= 0)
    balance = balance + amount
  }
}

除了不支撑承继,actorclass 十分类似:

  • 引证类型;

  • 能够遵守指定的协议;

  • 支撑 extension 等。

当然了,它们最大的差异在于 actor 内部完成了数据拜访的同步机制,如上图所示。

Actor isolation


所谓 Actor isolation 就是以 actor 实例为单元 (鸿沟),将其内部与外界隔离开。

严厉约束跨界拜访。

跨过 Actor isolation 的拜访称之为 cross-actor reference,如下图所示:

Swift 新并发框架之 actor
cross-actor reference 有 2 种状况:

  • 引证 actor 中的 『 不可变状况 (immutable state) 』,如上面比如中的accountNumber,因为其初始化后就不会被修正,也就不存在 Data races,故即使是跨界拜访也不会有问题;

  • 引证 actor 中的 『 可变状况 (mutable state)、调用其办法、拜访核算特点 』 等都被认为有潜在的 Data races,故不能像一般拜访那样。

    如前所述,Actor 内部有一个mailbox,专门用于接收此类拜访,并顺次串行履行它们,从而保证在并发下的数据安全。

    从这儿咱们也能够看出,此类拜访具有『 异步 』特征,即不会当即回来成果,需求排队顺次履行。

    因而,需求经过 await履行此类拜访,如:

    class AccountManager {
      let bankAccount = BankAccount.init(accountNumber: 123456789, initialDeposit: 1_000)
      func depoist() async {
        // 下面的 bankAccount.accountNumber、bankAccount.deposit(amount: 1) 都归于cross-actor reference
        // 对 let accountNumber 能够像一般特点那样拜访
        //
        print(bankAccount.accountNumber)
        // 而关于办法,不管是否是异步办法都需经过 await 调用
        //
        await bankAccount.deposit(amount: 1)
      }
    }
    

    当然,更不或许 cross-actor 直接修正 actor state:

      func depoist() async {
        // ❌ Error: Actor-isolated property 'balance' can not be mutated from a non-isolated context
        bankAccount.balance += 1
      }
    

nonisolated


Actor 内部经过 mailbox 机制完成同步拜访,必然会有必定的功能损耗。

然而,actor 内部的办法、核算特点并不必定都会引起 Data races。

为了处理这一矛盾,Swift 引入了要害字 nonisolated 用于润饰那些不会引起 Data races 的办法、特点,如:

extension BankAccount {
  // 在该办法内部只引证了 let accountNumber,故不存在 Data races
  // 也就能够用 nonisolated 润饰
  nonisolated func safeAccountNumberDisplayString() -> String {
    let digits = String(accountNumber)
    return String(repeating: "X", count: digits.count - 4) + String(digits.suffix(4))
  }
}
// 能够像一般办法一样调用,无需 await 入队
bankAccount.safeAccountNumberDisplayString()

当然了,在nonisolated办法中是不能拜访 isolated state 的,如:

extension BankAccount {
  nonisolated func deposit(amount: Double) {
    assert(amount >= 0)
    // Error: Actor-isolated property 'balance' can not be mutated from a non-isolated context
    balance = balance + amount
  }
}

在 actor 内部,不管是否是 nonisolated,各办法、特点都能够直接拜访,如:

extension BankAccount {
  // 在 deposit 办法中能够直接拜访、修正 balance
  func deposit(amount: Double) {
    assert(amount >= 0)
    balance = balance + amount
  }
}

但需求留意的是,正如前面所述,Actor isolation 是以 actor 实例为鸿沟,如下是有问题的:

extension BankAccount {
  func transfer(amount: Double, to other: BankAccount) throws {
    if amount > balance {
      throw BankAccountError.insufficientBalance(balance)
    }
    print("Transferring \(amount) from \(accountNumber) to \(other.accountNumber)")
    balance = balance - amount
    // Actor-isolated property 'balance' can not be mutated on a non-isolated actor instance
    // Actor-isolated property 'balance' can not be referenced on a non-isolated actor instance
    other.balance = other.balance + amount  // error: actor-isolated property 'balance' can only be referenced on 'self'
  }
}

other相关于self来说归于另一个 actor 实例,故不能直接跨界拜访。

Actor reentrancy


为了避免死锁、提升功能,Actor-isolated 办法是可重入的:

  • Actor-isolated 办法在显式声明为异步办法时,其内部或许存在暂停点;

  • 当 Actor-isolated 办法因暂停点而被挂起时,该办法是能够重入的,也就是在前一个挂起被恢复前能够再次进入该办法;

extension BankAccount {
  private func authorize() async -> Bool {
    // Simulate the authentication process
    //
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    return true
  }
  func withdraw(amount: Double) async throws -> Double {
    guard balance >= amount else {
      throw BankAccountError.insufficientBalance(balance)
    }
    // suspension point
    //
    guard await authorize() else {
      throw BankAccountError.authorizeFailed
    }
    balance -= amount
    return balance
  }
}
class AccountManager {
  let bankAccount = BankAccount.init(
    accountNumber: 123456789, 
    initialDeposit: 1000
  )
  func withdraw() async {
    for _ in 0..<2 {
      Task {
        let amount = 600.0
        do {
          let balance = try await bankAccount.withdraw(amount: amount)
          print("Withdrawal succeeded, balance = \(balance)")
        } catch let error as BankAccount.BankAccountError {
          switch error {
          case .insufficientBalance(let balance):
            print("Insufficient balance, balance = \(balance), withdrawal amount = \(amount)!")
          case .authorizeFailed:
            print("Authorize failed!")
          }
        }
      }
    }
  }
}
Withdrawal succeeded, balance = 400.0
Withdrawal succeeded, balance = -200.0

上述成果明显是不对的。

一般的,check—reference/change 二步操作不该跨 await suspension point。

因而,fix 也很简单,在真实 reference/change 前再 check 一次:

  func withdraw(amount: Double) async throws -> Double {
    guard balance >= amount else {
      throw BankAccountError.insufficientBalance(balance)
    }
    // suspension point
    //
    guard await authorize() else {
      throw BankAccountError.authorizeFailed
    }
    // re-check
    guard balance >= amount else {
      throw BankAccountError.insufficientBalance(balance)
    }
    balance -= amount
    return balance
  }
Withdrawal succeeded, balance = 400.0
Insufficient balance, balance = 400.0, withdrawal amount = 600.0!

总归,在开发过程中要留意 Actor reentrancy 的问题。

globalActor/MainActor


如前文所述,actor 是以其实例为界进行数据维护的。

但,如下,若需求对全局变量 globalVar、静态特点 currentTimeStampe、以及跨类型 (ClassA1ClassA2)/跨实例进行数据维护该如何做?

var globalVar: Int = 1
actor BankAccount {
  static var currentTimeStampe: Int64 = 0
}
class ClassA1 {
  var a1 = 0;
  func testA1() {}
}
class ClassA2 {
  var a2 = 1
  var a1: ClassA1
  init() {
    a1 = ClassA1.init()
  }
  func testA2() {}
}

这正是 globalActor 要处理的问题。

currentTimeStampe 虽界说在 actor BankAccount 中,但因为是 static 特点,故不在 actor 的维护范围内。 也就是不归于 BankAccount 的 actor-isolated 范围。

因而,能够在任意当地经过 BankAccount.currentTimeStampe 拜访、修正其值。

@globalActor
public struct MyGlobalActor {
  public actor MyActor { }
  public static let shared = MyActor()
}

如上,界说了一个 global actor:MyGlobalActor ,几个要害点:

  • global actor 的界说需求运用 @globalActor润饰;

  • @globalActor 需求完成 GlobalActor 协议:

    @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
    public protocol GlobalActor {
        /// The type of the shared actor instance that will be used to provide
        /// mutually-exclusive access to declarations annotated with the given global
        /// actor type.
        associatedtype ActorType : Actor
        /// The shared actor instance that will be used to provide mutually-exclusive
        /// access to declarations annotated with the given global actor type.
        ///
        /// The value of this property must always evaluate to the same actor
        /// instance.
        static var shared: Self.ActorType { get }
        /// The shared executor instance that will be used to provide
        /// mutually-exclusive access for the global actor.
        ///
        /// The value of this property must be equivalent to `shared.unownedExecutor`.
        static var sharedUnownedExecutor: UnownedSerialExecutor { get }
    }
    
  • GlobalActor 协议中,一般咱们只需完成 shared 特点即可 (sharedUnownedExecutorGlobalActor extension 中有默许完成);

  • global actor (本例中的MyGlobalActor) 本质上是一个 marker type,其同步功能是凭借 shared 特点供给的 actor 实例完成的;

  • global actor 可用于润饰类型界说 (如:class、struct、enum,但不能用于 actor)、办法、特点、Closure等。

    // 在闭包中的用法如下:
    Task { @MyGlobalActor in
      print("")
    }
    
@MyGlobalActor var globalVar: Int = 1
actor BankAccount {
  @MyGlobalActor static var currentTimeStampe: Int64 = 0
}
@MyGlobalActor class ClassA1 {
  var a1 = 0;
  func testA1() {}
}
@MyGlobalActor class ClassA2 {
  var a2 = 1
  var a1: ClassA1
  init() {
    a1 = ClassA1.init()
  }
  func testA2() {
    // globalVar、ClassA1/ClassA2 的实例、BankAccount.currentTimeStampe
    // 它们同归于 MyGlobalActor 的维护范围内
    // 故它们间的联系属 actor 内部联系,它们间能够正常拜访
    //
    globalVar += 1
    a1.testA1()
    BankAccount.currentTimeStampe += 1
  }
}
await globalVar
await BankAccount.currentTimeStampe

Swift 新并发框架之 actor

如上,能够经过 @MyGlobalActor 对它们进行数据维护,并在它们间形成一个以MyGlobalActor 为界的 actor-isolated:

  • MyGlobalActor 内部能够对它们进行正常拜访,如 ClassA2.testA2 办法所做;

  • MyGlobalActor 以外,需经过同步办法拜访,如:await globalVar

UI 操作都需求在主线程上履行,因而有了 MainAcotr,几个要害点:

  • MainActor 归于 globalAcotr 的特例;

    @globalActor final public actor MainActor : GlobalActor
    
  • 被 MainActor 润饰的办法、特点等都将在主线程上履行。

还记得在『 Swift 新并发结构之 async/await 』一文中说到的异步办法在暂停点前后或许会切换到不同线程上运行吗?

被 MainActor 润饰的办法是个破例,它必定是在主线程上履行。

除了用 @MainActor 特点外,咱们也能够经过 MainActor.run 在主线程上履行一段代码:

extension MainActor {
  /// Execute the given body closure on the main actor.
  public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T where T : Sendable
}

如:

await MainActor.run {
  print("")
}

谨防内部幺蛾子


至此,咱们知道 actor 是经过 mailbox 机制串行履行外部调用来保障数据安全。

弦外之音就是如果在 actor 办法内部存在 Data races,它是无能为力的,如:

1  actor BankAccount {
2    var balances: [Int: Double] = [1: 0.0]
3
4    func deposit(amount: Double) {
5      assert(amount >= 0)
6      for i in 0..<1000 {
7        // 在 actor 办法内部手动开启子线程
8        //
9        Thread.detachNewThread {
10         let b = self.balances[1] ?? 0.0
11         self.balances[1] = b + 1
12         print("i = \(i), balance = \(self.balances[1])")
13       }
14     }
15   }
16 }
17
18 class AccountManager {
19   let bankAccount = BankAccount.init(accountNumber: 123, initialDeposit: 1000, name: "Jike", age: 18)
20   func depoist() async {
21     await bankAccount.deposit(amount: 1)
22   }
23 }

如上面这段代码(成心伪造的),因为BankAccount.deposit 内部手动开启了子线程 (第 9 ~ 13 行),故存在 Data races 问题,会 crash。

一般地,actor 首要用作 Data Model,不该在其间处理大量业务逻辑。

尽量避免在其间手动开启子线程、运用GCD等,不然需求运用传统办法 (如 lock) 处理因而引起的多线程问题。

躲避外部陷阱


说完内忧,再看外患!

正如前文所讲,Actor 经过 mailbox 机制处理了外部调用引起的多线程问题。

可是…,关于外部调用就能够无忧无虑了吗?

class User {
  var name: String
  var age: Int
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}
actor BankAccount {
  let accountNumber: Int
  var balance: Double
  var name: String
  var age: Int
  func user() -> User {
    return User.init(name: name, age: age)
  }
}
class AccountManager {
  let bankAccount = BankAccount.init(accountNumber: 123, initialDeposit: 1000, name: "Jike", age: 18)
  func user() async -> User {
    // Wraning: Non-sendable type 'User' returned by implicitly asynchronous call to actor-isolated instance method 'user()' cannot cross actor boundary
    return await bankAccount.user()
  }
}

留意上面这段代码在编译时编译器给的 Warning:

Non-sendable type ‘User’ returned by implicitly asynchronous call to actor-isolated instance method ‘user()’ cannot cross actor boundary.

一切与 Sendable 相关的 warning 都需求 Xcode 13.3 才会报。

先抛开什么是 Sendable 不谈

这个 warning 还是很好理解的:

  • User 是引证类型(class);

  • 经过 actor-isolated 办法将 User 实例传递到了 actor 外面;

  • 此后,被传递出来的 user 实例天然得不到 actor 的维护,在并发环境下明显就不安全了。

经过参数跨 actor 鸿沟传递类实例也是相同的问题:

extension actor BankAccount {
  func updateUser(_ user: User) {
    name = user.name
    age = user.age
  }
}
extension AccountManager {
  func updateUser() async {
    // Wraning: Non-sendable type 'User' passed in implicitly asynchronous call to actor-isolated instance method 'updateUser' cannot cross actor boundary
    await bankAccount.updateUser(User.init(name: "Bob", age: 18))
  }
}

当然了,跨 actor 传递函数、闭包也是不可的:

extension BankAccount {
  func addAge(amount: Int, completion: (Int) -> Void) {
    age += amount
    completion(age)
  }
}
extension AccountManager {
  func addAge() 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)
    })
  }
}

除了这些 warning,还有名副其实的 crash:

extension User {
  func testUser(callback: @escaping () -> Void) {
    for _ in 0..<1000 {
      DispatchQueue.global().async {
        callback()
      }
    }
  }
}
extension BankAccount {
  func test() {
    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]))")
    }
  }
}

如上,尽管 BankAccountactor 类型,且其内部没有开启子线程等『 非法操作 』,

但在调用 User.testUser(callback: @escaping () -> Void) 后会 crash。

怎么办?

这时就要轮到 Sendable 上台了:『 Swift 新并发结构之 Sendable 』

小结

  • actor 是一种新的引证类型,旨在处理 Data Races;

  • actor 内部经过 mailbox 机制完成一切外部调用的串行履行;

  • 关于明确不存在 Data Races 的办法、特点能够运用nonisolated润饰使之成为『 惯例 』办法,以提升功能;

  • 经过 @globalActor 能够界说全局 actor,用于对全局变量、静态变量、多实例等进行维护;

  • actor 内部尽量避免开启子线程避免引起多线程问题;

  • actor 应作 Data Model 用,不宜在其间处理过多业务逻辑。

参考资料

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