美文网首页Swift
Swift Concurrency框架之Actor

Swift Concurrency框架之Actor

作者: zzzworm | 来源:发表于2023-02-22 17:54 被阅读0次

    文章系列:

    Swift Actors是swift 5.5新引入的,作为对Concurrency最重要的特性变更,Actor试图解决并行开发中常见的数据竞争问题。

    什么是Actor

    Actor的概念并不新鲜, Actor 模式是一个通用的并发编程模型,而非某个语言或框架所有,几乎可以用在任何一门编程语言中,最早Actor模型(Actor model)首先是由Carl Hewitt在1973定义, 由Erlang OTP 推广。Actor类似面向对象编程(OO)中的对象,每个Actor实例封装了自己相关的状态,并且和其他Actor处于物理隔离状态。Actor模型内部的状态由它自己维护即它内部数据只能由它自己修改(通过消息传递来进行状态修改),同时Actor内部是以单线程的模式来执行的,所以使用Actors模型进行并发编程可以很好地避免数据不同步的问题。
    在 Swift 当中,actor 包含 state、mailbox、executor 三个重要的组成部分,其中:

    • state 就是 actor 当中存储的值,它是受到 actor 保护的,访问时会有一些限制以避免数据竞争(data race)。
    • mailbox 字面意思是邮箱的意思,在这里我们可以理解成一个消息队列。外部对于 actor 的可变状态的访问需要发送一个异步消息到 mailbox 当中,actor 的 executor 会串行地执行 mailbox 当中的消息以确保 state 是线程安全的。
    • executor,actor 的逻辑(包括状态修改、访问等)执行所在的执行器。

    在Swift中定义一个Actor和定义一个Class是类似的,只是关键字由class改成了actor。Actor也同样支持构造器,属性和方法,也支持索引器。actor甚至支持protocol和模版元编程。不过在定义actor的属性时需要立即初始化构造。

    actor BankAccount {
        let accountNumber: String
        var balance: Double
    
        init(accountNumber: String, initialDeposit: Double) {
            self.accountNumber = accountNumber
            self.balance = initialDeposit
        }
    }
    

    Actor是一个引用类型,和struct值类型不同,Actor更像是确保了数据线程安全的 class,例如:
    let account = BankAccount(accountNumber: 1234, initialDeposit: 1000)
    let account2 = account
    print(account === account2) // true
    我们可以用类似于 class 的方式来构造 actor,并且创建多个变量指向同一个实例,以及使用 === 来判断是否指向同一个实例。程序运行时,我们也可以看到 account 和 account2 指向的地址是相同的:


    同时actor不支持继承。


    actor不支持继承

    actor如何解决数据竞争问题?

    以典型的银行账号系统为例,在以往的编程中,我们可以使用串行队列将所有的异步线程调用都在串行的队列中进行操作。

    final class BankAccountWithQueue {
        let accountNumber = "XXXXXXX"
    
        /// A combination of a private backing property and a computed property allows for synchronized access.
        private var _balance: Double = 0
        var balance: Double {
            queue.sync {
                _balance
            }
        }
    
        /// A concurrent queue to allow multiple reads at once.
        private var queue = DispatchQueue(label: "bank.deposit.queue", attributes: .concurrent)
    
        func deposit(amount: Double){
            /// Using a barrier to stop reads while writing
            queue.sync(flags: .barrier) {
                _balance += amount
            }
        }
    
        func withdraw(amount: Double) {
            /// Using a barrier to stop reads while writing
            queue.sync(flags: .barrier) {
                _balance -= amount
            }
        }
    
    }
    

    在actor中,不能直接修改通过修改属性方式来操作balance。Actor为了实现属性隔离,actor 的可变状态只能在 actor 内部被修改,同时要求对actor的状态修改都通过邮件方式,actor在收到邮件后会一一进行处理并异步返回结果(有点像我们上面的queue的实现)。
    针对BankAccout如果要进行存钱,函数实现如下:

    extension BankAccount {
        func deposit(amount: Double) async {
            assert(amount >= 0)
            balance = balance + amount
        }
    }
    

    现在我们可以通过代码来操作钱包账户了

    let account = BankAccount(accountNumber: 1234, initialDeposit: 1000)
    
    print(account.accountNumber) // OK,不可变状态
    print(await account.balance) // 可变状态的访问需要使用 await
    
    await account.deposit(amount: 90) // actor 的函数调用需要 await
    print(await account.balance)
    

    上面的代码可以发现accountNumber的访问可以直接进行,但是balance需要使用await调用,同样对于方法的调用由于函数签名为async,也需要进行await。实际上就是await调用封装了发邮件的过程。
    我们再来看一下转账的实现:

    extension BankAccount {
      enum BankError: Error {
        case insufficientFunds
      }
    
      func transfer(amount: Double, to other: BankAccount) async throws {
        assert(amount > 0)
    
        if amount > balance {
          throw BankError.insufficientFunds
        }
        balance = balance - amount
        // other.balance = other.balance + amount 错误示例
        await other.deposit(amount: amount) // OK
      }
    }
    

    account可以修改自己的balance但是不能修改other的balance。因为transfer函数处理邮件仅限作用于自己的邮件,如果要修改其他实例对象的状态只能调用其他实例对象的方法。可以看出actor的状态要求只能在自己的实例中修改,不能跨实例修改状态。
    Actor的属性默认都是隔离的,但有时候一些属性可能不需要进行保护,比如BankAccount的accountNumber在构造后就不可变。Swift也允许为Actor声明不需要隔离的属性:

    actor BankAccount {
        nonisolated let accountNumber: String
    }
    

    同时也可以用nonisolated修饰函数。但是nonisolated修饰的函数不能直接访问被隔离的状态,只能像外部函数一样使用await来异步访问。

    extension BankAccount : CustomStringConvertible {
        nonisolated var description: String {
            "Bank account #\(accountNumber)"
        }
        nonisolated func desc() async{
            print(self.accountNumber)
            print( await self.balance)
        }
    }
    

    @MainActor和main queue

    有了Concurrency中的Actor概念,SwiftUI 中也引入了@MainActor的装饰器,使用@MainActor装饰器可以让一个类或者函数都在主线程执行,使用MainActor.run()还可以将一些任务推送到主线程执行。

    async {
        await MainActor.run {
            // Perform UI updates
        }
    }
    

    这在开发UI关联状态Model的时候非常有用,在Combine中我们常常定义一个实现ObservableObject类对象,并用@Published来修饰可能会发生变化的状态属性,通过@MainActor装饰器可以保障我们的UI更新都是在主线程进行

    @MainActor
    class AccountViewModel: ObservableObject {
        @Published var username = "Anonymous"
        @Published var isAuthenticated = false
    }
    

    在SwitUI中Apple更进一步,对使用@StateObject@ObservedObject, Swift会确保其对UI的更新运行在Main Actor之上,这样你有时候在开发SwiftUI程序时不小心在异步线程更新了状态,SwitUI的body方法仍然会在主线程进行更新。

    struct ContentView: View {
        @StateObject private var accountViewModel = AccountViewModel()
    }
    

    虽然SwiftUI会对@StateObject@ObservedObject对象在body的方法更新上保障在主线程执行,仍然还是建议对UI所监听的对象添加@MainActor装饰器,这样可以保证所有对UI的修改能在主线程执行(不能保证其他非body方法没有对UI对象进行访问和修改),尤其针对一些从服务器返回数据的异步方法调用很有效果。

    相关文章

      网友评论

        本文标题:Swift Concurrency框架之Actor

        本文链接:https://www.haomeiwen.com/subject/nslakdtx.html