在Swift中使用Actors防止数据竞争

在Swift中使用Actors防止数据竞争

hudson 译 原文

数据竞争------所有开发人员最糟糕的噩梦!它们很难检测,非常不可预测,而且极难修复。苹果为开发人员提供了各种工具集,如NSLock和串行队列,以防止在运行时发生数据竞争,然而,它们都无法在编译时捕获竞争条件。随着Swift 5.5的发布,情况将不再如此!

Swift 5.5引入了Actor,这是新的Swift语言功能,可以帮助开发人员在开发期间捕捉任何可能的竞争条件。在本文中,我们将首先了解在使用调度队列(DispatchQueue)和异步任务时如何发生数据竞争。然后,我们将看看Actors如何帮助我们识别代码中的竞争条件,并一劳永逸地防止它们发生!

不要再浪费时间了,让我们直接开始吧。

数据竞争是如何发生的?

当2个或更多线程试图同时异步访问(读/写)相同的内存位置时,就会发生数据竞争。在Swift的上下文中,当我们尝试使用调度队列修改对象的状态时,通常会发生这种情况。我这是什么意思?

考虑以下Counter类,该类具有count计数变量,每次调用addCount()函数时,它都会增加1:

swift 复制代码
class Counter {
    
    var count = 0
    
    func addCount() {
        count += 1
    }
}

现在,假设我们有一个按钮,可以触发以下代码:

swift 复制代码
let totalCount = 1000
let counter = Counter()
let group = DispatchGroup()

// Call `addCount()` asynchronously 1000 times
for _ in 0..<totalCount {
    
    DispatchQueue.global().async(group: group) {
        counter.addCount()
    }
}

group.notify(queue: .main) {
    // Dispatch group completed execution
    // Show `count` value on label
    self.statusLabel.text = "\(counter.count)"
}

基本上,上述代码的作用是使用调度组异步调用CounteraddCount()函数1000次。一旦调度组执行完成,我们将在一个标签控件上显示Countercount的值。

理想情况下,每次点击按钮时,我们都应该看到标签上显示1000,但情况并非如此。我们得到的结果非常不一致,我们可能会偶尔得到1000,但我们得到的值通常小于1000。

正如您可能已经猜到的,这种不一致是由数据竞争引起的。当多个线程(由调度队列生成)尝试异步访问计数时,不能保证每个线程会一个接一个地更新计数值。因此,导致我们获得的最终结果非常不一致,很难预测。

专业提示:

Xcode有一个Thread Sanitizer ,可帮助开发人员以更一致的方式检测数据竞争。您可以通过导航到Product > Scheme > Edit Scheme... 来启用它。 之后,在编辑方案对话框中选择 运行>诊断> ,勾选Thread Sanitizer复选框。

现在您已经知道数据竞争是如何发生的,如果我们使用异步/等待和异步任务做同样的事情,会发生数据竞争吗?让我们来了解一下!

异步任务和数据竞争

在Swift并发领域,任务和任务组的工作原理与调度队列和调度组类似。我们可以通过创建一个父任务来实现之前的数据竞争条件,该任务生成一组异步执行addCount()函数的子任务。方法如下:

swift 复制代码
let totalCount = 1000
let counter = Counter()

// Create a parent task
Task {
    
    // Create a task group
    await withTaskGroup(of: Void.self, body: { taskGroup in
        
        for _ in 0..<totalCount {
            // Create child task
            taskGroup.addTask {
                counter.addCount()
            }
        }
    })
    
    statusLabel.text = "\(counter.count)"
}

在上面的代码中,我们使用withTaskGroup(of:body:)方法来创建任务组。在任务组中,我们创建1000个子任务,以异步执行addCount()函数。值得一提的是,withTaskGroup(of:body:)方法是可等待的,因此它将暂停,直到所有子任务完成。一旦发生这种情况,我们将在标签上显示计数值。

当我尝试运行上面的代码时,我得到的结果出乎意料地一致!每次代码完成执行时,我都可以看到标签上显示1000。这是否意味着当我们使用任务和任务组时,数据竞争不会发生?🤔

不幸的是,答案是否定的!

当我尝试在启用Thread sanitizer 的情况下运行上述代码时,我仍然会收到Thread sanitizer 警告,这表明确实发生了数据竞争。

如果是这样,那么我们为什么能够获得如此一致的结果呢?我的猜测是,苹果在优化整个Swift并发模块方面做得很好,因此它能够解决简单的数据竞争条件,就像我们在示例代码中一样。

使用调度队列时,我们可以通过使用串行调度队列来防止并发写入来避免数据竞争。如果我们使用异步任务,我们应该使用什么来防止并发写入?这就是引进Actor的原因。

充当救援队的Actor

Actor是Swift 5.5中引入的一项新语言功能,主要用于帮助开发人员在开发期间识别任何可能的数据竞争条件。正如您稍后将看到的,每当我们尝试编写可能导致数据竞争的代码时,编译器都会给我们一个编译错误。如果你不熟悉Actor的工作方式,你可以参考我之前的文章,其中谈到了Actor的基础知识。

现在让我们试着把Counter类换成一个actor。我们需要做的是用actor取代class ,仅此而已!

swift 复制代码
actor Counter {
    
    private(set) var count = 0
    
    func addCount() {
        count += 1
    }
}

在这个阶段,我们的示例代码将在我们尝试访问count变量的地方给出2个编译错误。

错误消息"Expression is 'async' but is not marked with 'await'"到底是什么意思?这意味着我们不能像这样简单地访问count变量!

由于Counter现在是一个actor ,它一次只允许1个异步任务访问其可变状态(count变量)。因此,如果我们要访问count变量,我们必须将两个访问点标记为await,指示如果有另一个任务访问count变量,这些访问点可能会暂停。

swift 复制代码
let totalCount = 1000
let counter = Counter()

// Create a parent task
Task {
    
    // Create a task group
    await withTaskGroup(of: Void.self, body: { taskGroup in
        
        for _ in 0..<totalCount {
            // Create child task
            taskGroup.addTask {
                // Marked with await
                await counter.addCount()
            }
        }
    })
    
    // Marked with await
    statusLabel.text = "\(await counter.count)"
}

值得一提的是,actors将保护其可变状态免受[多线索]读写访问。这就是为什么我们的示例代码中的两个访问点上都出现了编译错误。

情况就是这样,我们介绍了如何通过使用Actor来防止数据竞争赛。如果你想亲自尝试示例代码,请在这里 随时获取。

小结

在Swift中引入Actors绝对是值得欢迎的。它使我们能够用很少的编码工作来编写更安全的异步代码。事实上,它是一个语言功能,使它能够在编译时捕获任何可能的竞争条件,从而防止我们意外地将数据竞争引起的错误发送给我们心爱的应用程序用户。

感谢您的阅读。👨🏻‍💻

相关推荐
大熊猫侯佩14 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩2 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu2 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩2 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩3 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩3 天前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩3 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple