Swift Concurrency框架之Actor

文章系列:

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对象进行访问和修改),尤其针对一些从服务器返回数据的异步方法调用很有效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容