前言

Swift Actors 是 Swift 5.5 中的新内容,也是 WWDC 2021 上并发重大变化的一部分。在有 actors 之前,数据竞赛是一个常见的意外状况。因而,在咱们深入研究具有阻隔和非阻隔拜访的行为体之前,最好先了解什么是数据竞赛,并了解当时你怎么处理这些问题。

Swift 中的 Actors 旨在完全处理数据竞赛问题,但重要的是要明白,很或许还是会遇到数据竞赛。本文将介绍 Actors 是怎么工作的,以及你怎么在你的项目中运用它们。

什么是 Actors?

Swift 中的 Actor 并不新鲜:它们受到 Actor Model 的启示,该模型将行为视为并发核算的通用基元。然后,SE-0306提案引进了 Actor,并解说了它们处理了哪些问题:数据竞赛。

当多个线程在没有同步的状况下拜访同一内存,并且至少有一个拜访是写的时分,就会产生数据竞赛。数据竞赛会导致不可猜测的行为、内存损坏、不稳定的测试和古怪的溃散。你或许会遇到无法处理的溃散,由于你不知道它们何时产生,怎么重现它们,或者怎么根据理论来修正它们。

Swift 中的 Actors 能够保护他们的状况免受数据竞赛的影响,并且运用它们能够让编译器在编写应用程序时为咱们提供有用的反应。此外,Swift 编译器能够静态地强制履行 Actors 顺便的限制,并避免对可变数据的并发拜访。

您能够运用 actor 关键字定义一个 Actor,就像您运用类或结构体相同:

actor ChickenFeeder {
    let food = "worms"
    var numberOfEatingChickens: Int = 0
}

Actor 和其他 Swift 类型相同,它们也能够有初始化器、办法、特点和子标号,一起你也能够用协议和泛型来运用它们。此外,与结构体不同的是:当你定义的特点需求手动定义时,actor 需求自定义初始化器。最终,重要的是要认识到 actor 是引证类型。

Actor 是引证类型,但与类比较依然有所不同

Actor 是引证类型,简而言之,这意味着副本引证的是同一块数据。因而,修正副本也会修正原始实例,由于它们指向同一个共享实例。你能够在我的文章Swift 中的 Struct 与 class 的区别中了解更多这方面的信息。

可是,与类比较,Actor 有一个重要的区别:他们不支撑承继。

Swift 中的 Actors 使用以及如何防止数据竞争

Swift 中的 Actor 几乎和类相同,但不支撑承继。

不支撑承继意味着不需求像便当初始化器和必要初始化器、重写、类成员或 openfinal 句子等功能。

可是,最大的区别是由 Actor 的主要职责决定的,即阻隔对数据的拜访。

怎么避免数据竞赛

Actors 怎么经过同步来避免数据竞赛

Actor 经过创立对其阻隔数据的同步拜访来避免数据竞赛。在Actors之前,咱们会运用各种锁来创立相同的成果。这种锁的一个例子是并发调度行列与处理写拜访的屏障相结合。受我在Concurrent vs. Serial DispatchQueue: Concurrency in Swift explained一文中解说的技术的启示。我将向你展现运用 Actor 的前后对比。

在 Actor 之前,咱们会创立一个线程安全的小鸡喂食器,如下所示:

final class ChickenFeederWithQueue {
    let food = "worms"
    /// 私有支撑特点和核算特点的组合允许同步拜访。
    private var _numberOfEatingChickens: Int = 0
    var numberOfEatingChickens: Int {
        queue.sync {
            _numberOfEatingChickens
        }
    }
    /// 一个并发的行列,允许一起进行屡次读取。
    private var queue = DispatchQueue(label: "chicken.feeder.queue", attributes: .concurrent)
    func chickenStartsEating() {
        /// 运用栅门阻挠写入时的读取
        queue.sync(flags: .barrier) {
            _numberOfEatingChickens += 1
        }
    }
    func chickenStopsEating() {
        /// 运用栅门阻挠写入时的读取
        queue.sync(flags: .barrier) {
            _numberOfEatingChickens -= 1
        }
    }
}

正如你所看到的,这儿有相当多的代码需求维护。在拜访非线程安全的数据时,咱们必须仔细考虑自己运用行列的问题。需求一个栅门标志来停止读取并允许写入。再一次,咱们需求自己来处理这个问题,由于编译器并不强制履行它。最终,咱们在这儿运用了一个 DispatchQueue,可是经常有围绕着哪个锁是最好的争论。

为了看清这一点,咱们能够运用咱们先前定义的 Actor 小鸡喂食器来完成上述例子:

actor ChickenFeeder {
    let food = "worms"
    var numberOfEatingChickens: Int = 0
    func chickenStartsEating() {
        numberOfEatingChickens += 1
    }
    func chickenStopsEating() {
        numberOfEatingChickens -= 1
    }
}

你会注意到的榜首件事是,这个实例更简单,更简单阅览。所有与同步拜访有关的逻辑都被隐藏在Swift规范库中的完成细节里。可是,最有趣的部分产生在咱们试图运用或读取任何可变特点和办法的时分:

Swift 中的 Actors 使用以及如何防止数据竞争

Actors 中的办法是阻隔的,以便同步拜访。

在拜访可变特点 numberOfEatingChickens 时,也会产生相同的状况:

Swift 中的 Actors 使用以及如何防止数据竞争

可变的特点只能从 Actor 内部拜访。

可是,咱们被允许编写以下代码:

let feeder = ChickenFeeder()
print(feeder.food) 

咱们的喂食器上的 food 特点是不可变的,因而是线程安全的。没有数据竞赛的危险,由于在读取过程中,它的值不能从另一个线程中改动。

可是,咱们的其他办法和特点会改动一个引证类型的可变状况。为了避免数据竞赛,需求同步拜访,允许按次序拜访。

运用 async/await 拜访数据

运用 async/await 从 Actors 拜访数据

在 Swift 中,咱们能够经过运用 await 关键字来创立异步拜访:

let feeder = ChickenFeeder()
await feeder.chickenStartsEating()
print(await feeder.numberOfEatingChickens) // Prints: 1 

避免不必要的暂停

在上面的例子中,咱们正在拜访咱们 Actor 的两个不同部分。首要,咱们更新吃食的鸡的数量,然后咱们履行另一个异步任务,打印出吃食的鸡的数量。每个 await 都会导致你的代码暂停,以等候拜访。在这种状况下,有两个暂停是有意义的,由于两部分其实没有什么共同点。可是,你需求考虑到或许有另一个线程在等候调用 chickenStartsEating,这或许会导致在咱们打印出成果的时分有两只吃食的鸡。

为了更好地理解这个概念,让咱们来看看这样的状况:你想把操作合并到一个办法中,以避免额外的暂停。例如,设想在咱们的 actor 中有一个告知办法,告知观察者有一只新的鸡开端吃东西:

extension ChickenFeeder {
    func notifyObservers() {
        NotificationCenter.default.post(name: NSNotification.Name("chicken.started.eating"), object: numberOfEatingChickens)
    }
} 

咱们能够经过运用 await 两次来运用此代码:

let feeder = ChickenFeeder()
await feeder.chickenStartsEating()
await feeder.notifyObservers() 

可是,这或许会导致两个暂停点,每个 await 都有一个。相反,咱们能够经过从 chickenStartsEating 中调用 notifyObservers 办法来优化这段代码:

func chickenStartsEating() {
    numberOfEatingChickens += 1
    notifyObservers()
} 

由于咱们现已在 Actor 内有了同步的拜访,咱们不需求另一个等候。这些都是需求考虑的重要改进,由于它们或许会对功能产生影响。

非阻隔(nonisolated)拜访

Actor 内的非阻隔(nonisolated)拜访

了解 Actor 内部的阻隔概念很重要。上面的例子现已展现了怎么经过要求运用 await 从外部参与者实例同步拜访。可是,如果您仔细观察,您或许现已注意到咱们的 notifyObservers 办法不需求运用 await 来拜访咱们的可变特点 numberOfEatingChickens

当拜访 Actor 中的阻隔办法时,你根本上能够拜访任何其他需求同步拜访的特点或办法。因而,你根本上是在重复运用你给定的拜访,以获得最大的收益。

可是,在有些状况下,你知道不需求有阻隔的拜访。actor 中的办法默认是阻隔的。下面的办法只拜访咱们的不可变的特点 food,但依然需求 await 拜访它:

let feeder = ChickenFeeder()
await feeder.printWhatChickensAreEating() 

这很古怪,由于咱们知道,咱们不拜访任何需求同步拜访的东西。SE-0313的引进正是为了处理这个问题。咱们能够用 nonisolated 关键字符号咱们的办法,告知 Swift编 译器咱们的办法没有拜访任何阻隔数据:

extension ChickenFeeder {
    nonisolated func printWhatChickensAreEating() {
        print("Chickens are eating \(food)")
    }
}
let feeder = ChickenFeeder()
feeder.printWhatChickensAreEating() 

注意,你也能够对核算的特点运用 nonisolated 的关键字,这对完成 CustomStringConvertible 等协议很有协助:

extension ChickenFeeder: CustomStringConvertible {
    nonisolated var description: String {     
        "A chicken feeder feeding \(food)"   
    } 
}

可是,在不可变的特点上定义它们是不需求的,由于编译器会告知你:

Swift 中的 Actors 使用以及如何防止数据竞争

将不可变的特点符号为 nonisolated 是多余的。

为什么会呈现数据竞赛

为什么在运用 Actors 时仍会呈现数据竞赛?

当在你的代码中继续运用 Actors 时,你肯定会下降遇到数据竞赛的危险。创立同步拜访能够避免与数据竞赛有关的古怪溃散。可是,你显然需求继续地运用它们来避免你的应用程序中呈现数据竞赛。

在你的代码中依然或许呈现竞赛条件,但或许不再导致异常。认识到这一点很重要,由于Actors 毕竟被宣传为能够处理一切问题的工具。例如,幻想一下两个线程运用 await正确地拜访咱们的 Actor 的数据:

queueOne.async {
    await feeder.chickenStartsEating()
}
queueTwo.async {
    print(await feeder.numberOfEatingChickens)
} 

这儿的竞赛条件定义为:“哪个线程将首要开端阻隔拜访?”。所以根本上有两种成果:

  • 行列一在先,添加吃食的鸡的数量。行列二将打印:1
  • 行列二在先,打印出吃食的鸡的数量,该数量仍为:0

这儿的不同之处在于咱们在修正数据时不再拜访数据。如果没有同步拜访,在某些状况下这或许会导致无法意料的行为。

定论

Swift Actors 处理了用 Swift 编写的应用程序中常见的数据竞赛问题。可变数据是同步拜访的,这保证了它是安全的。咱们还没有介绍 MainActor 实例,它本身便是一个主题。我将保证在以后的文章中介绍这一点。希望您能够跟随并知道怎么在您的应用程序中运用 Actor。

本文正在参与「金石方案」