Swift Actors: 防止数据竞争

什么是Actor

actor和类一样是引用类型,可以使用构造器,下标,属性,方法。和类不一样的是,actor同一时间只允许一个任务访问他的可变状态,这使得多个任务中的代码可以安全地与同一个actor实例进行交互。这种安全性由编译器强制实现的,而不是需要开发人员使用诸如锁之类的系统编写样板代码来实现。

swift 复制代码
actor Company {
    let name: String = "某T"
    var numberOfEmployees: Int
    init(numberOfEmployees: Int) {
        self.numberOfEmployees = numberOfEmployees
    }
    // 实施降本增效计划
    func doProfitImprovementPlan(){
        layoff(100)
        recruit(10)
        print(numberOfEmployees)
    }
}

Actor 隔离

Actor 隔离是用来保护它的可变状态,通过self直接引用和修改它的存储属性。

swift 复制代码
extension Company {
    func merge(with company: Company) {
        // error: Actor-isolated property 'numberOfEmployees' can not be referenced on a non-isolated actor instance
        let number = company.numberOfEmployees 
        numberOfEmployees += number
    }
}

如果 Companyclass, 这个方法不会报错,但是你需要加入锁机制避免造成数据竞争。

对于Actor,如果你尝试引用 company.numberOfEmployees会导致编译错误,因为numberOfEmployees 被隔离在actor 上下文中,只能被self访问。

一个actor实例上的所有声明都是默认actor-isolatedactor-isolated的声明能自由访问同一个actor实例的其他actor-isolated声明。任何不是actor-isolated的声明称为non-isolatednon-isolated的声明同步访问actor-isolated的声明。

actor外部引用该actoractor-isolated的声明,称为cross-actor引用。这样的引用有两种方式被允许。

  1. 引用不可变状态。

    这是因为不可变状态不会被修改,所有不会造成数据竞争。

    print(company.name)

  2. 使用异步函数调用

    通过异步函数的调用进行的引用。此类异步函数调用会转换成消息。这些消息存储在actor的邮箱中,发起异步函数调用的地方可能会被挂起,直到actor能处理邮箱中对应的消息。actor 一次只能处理一个消息,因此给定的actor永远不可能有两个并发执行actor隔离代码的任务。这确保了在actor隔离的可变状态不会发生数据竞争。

改正以上版本

swift 复制代码
extension Company {
    //改为异步函数
    func merge(with company: Company) async {
      	// cross-actor 引用
        let number = await company.numberOfEmployees 
        numberOfEmployees += number
    }
}

actor外部不能修改它的不可变状态,会导致编译时错误。

swift 复制代码
Task {
    await company.numberOfEmployees += 10
    //error: Actor-isolated property 'numberOfEmployees' can not be mutated from a non-isolated context
}

actor内部的方法可以定义成异步方法

swift 复制代码
extension Company {
    // 通过self引用可修改可变属性
    func recruit(_ numberToRecurit: Int) async {
        self.numberOfEmployees += numberToRecurit
    }
}

也可以将这个方法定义成同步方法

swift 复制代码
extension Company {
    // 通过self引用可修改可变属性
    func recruit(_ numberToRecurit: Int) {
        self.numberOfEmployees += numberToRecurit
    }
}

我们还可以通过nonisolated 标记非隔离只读属性,这是因为只读属性不会造成数据竞争。

swift 复制代码
extension Company {
    nonisolated var internationalName: String {
        return "T"
    }
}

可重入性

可重入性是指在一个 actor 处理一个异步方法时,可以暂停当前的任务去处理其他任务,完成后再回到之前的任务继续执行。这种机制允许 actor 更灵活和高效地处理并发任务,提高资源利用率,并且不会有死锁的风险。

但是我们需要注意的是,可重入性会导致在暂停点之前的状态和暂停点之后的状态不一致。

swift 复制代码
actor Account {
    private var balance: Int
    init(balance: Int) {
        self.balance = balance
    }
    func withdraw(amount: Int) async {
        // 检查余额
        guard balance >= amount else {
            return
        }
        // 可能的暂停点
        guard await authenticate() else {
            return
        }
        // 此时我们不能保证在暂停点后能够取钱,我们必须再检查一次余额。
        guard balance >= amount else {
            return
        }
        balance -= amount
        print("balance: \(balance)")
    }
    
    func authenticate() async -> Bool {
        try? await Task.sleep(nanoseconds: 1_000_000_000)
        return true
    }
}

// 客户端调用
let account = Account(balance: 5)
for _ in 0..<12{
    Task {
        await account.withdraw(amount:1)
    }
}

考察账户程序的withdraw(:)函数,如果我们没有在authenticate()之后检查余额,很可能会造成余额为负数的情况。这是因为当一个任务执行运行至暂停点时,另外一个任务执行异步函数也来到暂停点,由于之前authenticate()还没有处理完,此时新的任务也将在暂停点等待,然后前一个任务在暂停点恢复取款,接着新的任务在暂停点恢复取款,此时如果没有判断是否可以继续取款,有可能会造成余额为负数的情况。

@globalActor

在 Swift 中,Global Actors 是用于确保某些代码片段总是在同一个特定的执行上下文中运行的全局抽象。Global Actors 提供了一种简单的方法来确保线程安全性和同步性,特别是在处理共享状态和资源时。在任何使用 global actor 属性的地方,你都将通过共享的 actor 实例确保同步,以确保对声明的互斥访问。

swift 复制代码
@globalActor
actor DataManagerActor {
    static let shared = DataManagerActor()
}

@DataManagerActor
class Counter {
    private var value = 0
    func increment() {
        value += 1
        print(value)
    }
}

func doCount() async {
    let counter = await Counter()
    for _ in 0..<6 {
        Task {
            await counter.increment()
        }
    }
}

Task {
    await doCount()
}

@MainActor

@MainActor 是 Swift Concurrency 中的一个关键特性,它保证被标注的代码在主线程上运行。这个特性对于需要在主线程上执行的操作(如更新用户界面)非常重要。

  • 保证在主线程上执行:所有标注了 @MainActor 的代码都将确保在主线程上执行。这对于 UI 更新和其他需要在主线程上进行的任务是必要的。主线程是应用程序的主执行线程,通常处理用户输入和 UI 更新。如果在后台线程上执行 UI 更新操作,会导致不可预测的行为和崩溃。

  • 线程安全:通过使用 @MainActor,可以确保代码在一个单一线程上执行,从而避免多线程环境下的竞争条件和数据竞态。

  • 简化代码:使用 @MainActor 可以使代码更加简洁和易读,因为不需要手动切换到主线程。例如,在没有 @MainActor 的情况下,需要显式地使用 DispatchQueue.main.async 来切换到主线程。

swift 复制代码
//保证运行在主线程
@MainActor func updateUI(text: String) {
    self.titleLabel.stringValue = text
}

Task {
      let data = Task {
          await fetchData(from:"www.apple.com")
      }
      let result = await data.value
      //保证运行在主线程
      await MainActor.run {
          self.titleLabel.stringValue = result
      }
  }

结语

希望这篇文章能帮助你更好地理解和运用 Swift 的 actor 模型,提高你的并发编程能力。如果你有任何问题或建议,欢迎在评论区留言讨论。祝你编程愉快!

相关推荐
良技漫谈19 小时前
Rust移动开发:Rust在iOS端集成使用介绍
后端·程序人生·ios·rust·objective-c·swift
KeithTsui2 天前
ZFC in LEAN 之 前集的等价关系(Equivalence on Pre-set)详解
开发语言·其他·算法·binder·swift
袁代码2 天前
Swift 开发教程系列 - 第4章:函数与闭包
ios·swift·ios开发
安泽13143 天前
高德地图美食
开发语言·swift·美食
袁代码3 天前
Swift 开发教程系列 - 第2章:Swift 基础语法
swift·ios开发·基础教程
袁代码3 天前
Swift 开发教程系列 - 第1章:Swift 简介与开发环境配置
swift·ios开发·基础教程
孚亭4 天前
一些swift问题
swift
莫问alicia4 天前
echarts 实现3D饼状图 加 label标签显示
前端·3d·echarts·swift
uiop_uiop_uiop6 天前
iOS Swift5算法恢复——HMAC
ios·iphone·swift
東三城7 天前
【ios】---SwiftUI开发从入门到放弃
ios·swiftui·swift·1024程序员节