Swift并发模型中的Actor
Swift并发模型中的Actor

Swift并发模型中的Actor

actor 是 Swift Concurrency 中一个非常重要的概念,用于解决多线程并发中的数据安全问题。

actor 是一种保护内部状态免受多线程同时访问破坏的并发类型。
它自动序列化内部访问,不需要手动加锁,但它是并发安全的。

Actor 隔离

Actor 隔离是 Swift 中的一种并发控制机制,用来确保数据安全地在多线程环境下访问和修改。它是通过引入 actor 这种类型来实现的。actor 提供了一种方式,能够在多线程并发编程中避免数据竞争(data race),从而确保数据访问的安全性和一致性。

在 Swift 中,actor 就像类一样,是一种引用类型,但它有一个重要的特性:actor 隔离。这个特性确保 actor 内部的数据只能被同一个 actor 本身直接访问,其他代码要想访问它,必须通过异步调用。这种设计可以有效地避免并发环境下的竞争条件和数据不一致的问题。

定义和使用 Actor

可以像定义类一样定义一个 actor:

actor BankAccount {
    var balance: Double = 0.0
    func deposit(amount: Double) {
        balance += amount
    }
    func withdraw(amount: Double) {
        if amount <= balance {
            balance -= amount
        }
    }
}

在这个例子中,BankAccount 是一个 actor,它有一个属性 balance 和两个方法 deposit 和 withdraw。由于 actor 的线程安全特性,我们可以确保在并发环境下 balance 始终保持一致。

如果将actor更换为class,那么就是一个普通的类。

调用actor:

let account = BankAccount() // 创建 actor 实例
Task {
    await account.deposit(amount: 100)
    await account.withdraw(amount: 50)
    print(await account.balance) // 50.0
}

创建actor实例和类实例类似,由于 actor 的内部状态是隔离的,因此在访问 actor 的方法或属性时,通常需要使用 await 进行异步调用。

如果在同步函数里,想调用 async 函数,就要用 Task 包裹。

注意:属性和方法都需要使用await,而不单单是方法。

在actor内部可以直接访问方法和属性,而不需要使用await,因为它们同于同一个线程上下文,不涉及并发。

actor 内部的数据只能被同一个 actor 本身直接访问。

actor BankAccount {
    private var balance: Double = 0.0

    func deposit(amount: Double) {
        balance += amount
    }

    func getBalance() -> Double {
        return balance
    }
}

let account = BankAccount()

Task {
    await account.deposit(amount: 100.0)  // 外部访问,需要 await
    let balance = await account.getBalance()  // 外部访问,需要 await
    print(balance)
}

在上面的 BankAccount 例子中,balance 是一个 private 属性,只能通过 actor 自己的 deposit 和 getBalance 方法访问。外部任务如果想要查看或修改 balance,必须通过异步调用 deposit 或 getBalance,并使用 await 关键字。

这种设计保证了 actor 内部状态不会受到外部直接修改的影响,从而避免了数据竞争和状态不一致的问题。

Actor特点

1、封装和隔离:actor 的内部状态(比如属性)默认是“隔离的”,外部代码无法直接访问或修改。所有对 actor 内部状态的访问都必须通过 actor 提供的异步方法。

2、异步访问:当其他代码(在不同的线程上)需要访问 actor 的数据时,必须使用 await,以确保调用是线程安全的。这是因为 actor 会在内部处理这些访问请求,使其逐个执行,从而避免数据竞争。

let counter = Counter()

// 这会报错,因为不能直接访问隔离的属性
print(counter.count) // ❌

// 必须通过异步调用
Task {
    print(await counter.count) // ✅
}

3、线程安全:actor 自动处理线程同步,不需要手动使用锁或者GCD等其他同步机制。因为 actor 的隔离机制保证了对内部状态的安全访问。

4、并发安全:所有内部var只能通过actor的方法访问,不会发生数据竞争。

5、自动串行化:多个任务同时调用actor的方法,内部排队执行。

使用示例

假设我们有一个计数器类,如果没有使用 actor,在多线程情况下可能会导致数据竞争:

class Counter {
    var value = 0
    func increment() {
        value += 1
    }
}

在多线程访问时,value 的操作就可能发生数据竞争,导致计数不准确。

使用 actor 来解决:

actor Counter {
    var value = 0
    func increment() {
        value += 1
    }
}

let counter = Counter()
await counter.increment()

Actor和Class的关系

actor 是 Swift 语言引入的一种新的引用类型,类似于 class,但具备并发安全特性。

actor 是一种类似 class 的引用类型,不是 struct,也不是 class 的子类型,但在概念上更接近 class。

相同点

1、Actor和Class都是引用类型。

2、Actor和Class都可以继承,Actor只支持协议,不可以继承另一个Actor。

3、Actor和Class都可以使用init()构造器。

4、Actor和Class都可以引用语义。

不同点

1、Class不支持并发安全,会有数据竞争的问题,Actor自动处理并发访问,保证线程安全。

2、Class不可以多线程访问成员,可能崩溃,Actor则可以安全的内部排队执行。

3、Class可以直接访问属性,Actor由于自身的隔离特性,外部访问Actor的属性和方法时,需要使用await进行异步调用。

4、Class需要手动枷锁,Actor 内部使用了锁机制来保证操作的顺序性。

示例代码:

actor Counter {
    private var value = 0
    
    func increment() {
        value += 1
    }

    func getValue() -> Int {
        value
    }
}

Actor不能像Class一样直接访问value,因此必须:

let counter = Counter()
await counter.increment()
let v = await counter.getValue()

此外,Actor并不能完全替代Class,虽然两者都是引用类型,但是Actor主要用于保护共享状态的并发常见。

因此,不要把所有的Class替换成Actor,而是只在需要多线程访问同一个对象时,才使用Actor。

Actor的 nonisolated 修饰符

如果希望在 actor 中定义一个方法,但希望它可以从外部同步调用,可以使用 nonisolated 修饰符:

actor Logger {
    func log(message: String) {
        print("Log: \(message)")
    }
    
    nonisolated func getCurrentTime() -> String {
        return Date().description
    }
}

let logger = Logger()

// 可以同步调用 nonisolated 方法
print(logger.getCurrentTime()) // 不需要 await

在上面的例子中,getCurrentTime 被标记为 nonisolated,因此可以直接同步调用而无需 await。

注意:actor 的属性或方法都可以标记为 nonisolated,这表示它们不受 actor 的隔离约束。标记为 nonisolated 的属性或方法可以像普通的类属性或方法一样访问,不需要 await。

总结

Actor 是 Swift 为了解决并发数据竞争而引入的一种类型,它就像是线程安全的类(class),内部状态只能通过异步访问,自动保护数据不被多线程破坏。

扩展知识

@State中的Actor隔离

1、@State 是 View 的私有状态

在 SwiftUI 中,@State 属性是专门为视图设计的,它允许视图拥有自己的私有状态。在视图生命周期内,这些属性可以用来存储和更新数据。

视图会观察 @State 的变化,并在状态更新时自动重新渲染视图。

2、线程安全

Swift 的并发模型会自动保证 @State 的访问是安全的。这意味着即使多个任务(或线程)尝试访问或更新 @State,系统也会确保操作的正确性,避免数据竞争。

从技术上讲,可以认为 SwiftUI 视图的 @State 属性是与视图隔离的,类似于 actor 的概念:actor 负责其内部数据的隔离与保护,而 @State 也是被隔离和保护的。

3、避免数据竞争

数据竞争(data race)指的是多个线程同时访问并修改数据,导致数据不一致。actor 和 SwiftUI 中的 @State 都通过线程安全的机制,确保数据竞争不会发生。

比如,在 SwiftUI 中如果一个 @State 属性在主线程更新,那么不需要担心其他地方访问 @State 会出现竞争情况,因为 SwiftUI 的机制会处理这些更新,确保它们按顺序执行。

举例说明:

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

在这个例子中,count 是一个 @State 属性。虽然可能有多个操作(例如用户点击按钮,导致 count 变化),SwiftUI 会确保 count 的状态更新是线程安全的。

当按钮点击时,SwiftUI 会自动管理 @State,并决定什么时候更新视图,不会出现并发问题。这种机制与 actor 的隔离性类似,@State 是由 SwiftUI 保护和管理的,确保不会有意外的并发访问。

@State 属性被称为 “actor 隔离的”,意味着 SwiftUI 自动处理对这些属性的访问,使它们在并发环境下也是安全的。虽然不是直接使用 actor 机制,但 SwiftUI 的设计理念和 actor 类似,都保证了数据的隔离性和安全性。

相关文章

1、Swift并发模型中的Task:https://fangjunyu.com/2025/05/15/swift%e5%b9%b6%e5%8f%91%e6%a8%a1%e5%9e%8b%e4%b8%ad%e7%9a%84task/

2、Swift UI 深入理解async和await:https://fangjunyu.com/2024/10/12/swift-ui-%e6%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3async%e5%92%8cawait/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注