Sendable协议如何帮助防止数据竞争
hudson 译 原文
在我之前的文章中,您了解到,actor可以通过确保其可变状态互斥来帮助我们防止数据竞争。这种说法是正确的,条件是我们在actor内部访问其可变状态。如果可变状态可以在actors之外访问,数据竞争仍然可能发生!
在本文中,让我们探索这种数据竞争如何发生,以及Sendable
协议如何帮助防止这种情况发生。除此之外,我们还将看看苹果未来将为Sendable
带来的改进,以应对这种情况。
所以,不用多说,让我们开始吧!
从actor外传递数据时的数据竞争
假设我们有一个Article
文章类,它有一个likeCount
变量,可以跟踪文章从读者那里获得的点赞数量。
swift
final class Article {
let title: String
var likeCount = 0
init(title: String) {
self.title = title
}
}
我们还有一个ArticleManager
actor ,负责管理一系列文章:
swift
actor ArticleManager {
private let articles = [
Article(title: "Swift Senpai Article 01"),
Article(title: "Swift Senpai Article 02"),
Article(title: "Swift Senpai Article 03"),
]
/// Increase like count by 1
func like(_ articleTitle: String) {
guard let article = getArticle(with: articleTitle) else {
return
}
article.likeCount += 1
}
/// Get article based on article title
func getArticle(with articleTitle: String) -> Article? {
return articles.filter({ $0.title == articleTitle }).first
}
}
请注意,ArticleManager
演员有一个like(_:)
函数,可以增加特定文章的likeCount
,以及一个getArticle(with:)
函数,可以根据给定的文章标题返回文章。
由于存在getArticle(with:)
功能,ArticleManager
的文章现在可以在actor之外访问。换句话说,actor的可变状态现在可以在actor之外更新,从而存在潜在数据竞争。
现在,考虑以下位于actor之外dislike(_:)
函数:
swift
let manager = ArticleManager()
/// Access article outside of the actor and reduces its like count by 1
func dislike(_ articleTitle: String) async {
guard let article = await manager.getArticle(with: articleTitle) else {
return
}
// Reduce like count
article.likeCount -= 1
}
由于我们现在在actor之外减少(修改)文章的点赞数,如果我们尝试同时运行actor的like(_:)
和上述 dislike(_:)
函数,将发生数据竞争!
swift
let articleTitle = "Swift Senpai Article 01"
// Create a parent task
Task {
// Create a task group
await withTaskGroup(of: Void.self, body: { taskGroup in
// Create 3000 child tasks to like
for _ in 0..<3000 {
taskGroup.addTask {
await self.manager.like(articleTitle)
}
}
// Create 1000 child tasks to dislike
for _ in 0..<1000 {
taskGroup.addTask {
await self.dislike(articleTitle)
}
}
})
print("👍🏻 Like count: \(await manager.getArticle(with: articleTitle)!.likeCount)")
}
在上述代码中,我们创建了3000个子任务来点赞一篇文章,1000个子任务来同时点赞和踩同一篇文章。最后,即使我们能够获得"👍🏻like count:2000"
的输出,Xcode的 thread sanitizer仍然会显示线程问题,这表明数据竞争确实发生了。
专业提示:
您可以通过导航到Product>Schema>Edit Scema 来启用Thread Sanitizer ...之后,在Edit Schema 对话框中选择Run>Diagnostic> 勾选Thread Sanitizer复选框。
既然你已经看到了在actor之外修改actor状态如何导致数据竞争,我们可以做些什么来防止这种情况发生?
通过添加一致性来检查可发送
Sendable
是Swift 5.5中引入的一种新协议,与async/await和actors一起引入。Sendable
类型的值可以在不同的actors之间共享。如果我们将一个值从一个地方复制到另一个地方,并且两个地方都可以安全地修改该值的副本,而不会相互干扰,那么该类型将被视为Sendable
类型。要了解更多信息,您可以看看这个WWDC视频。
回到我们的示例代码,为了避免在ArticleManager
之外修改文章时出现数据竞争,我们必须使Article
类型符合Sendable
协议。我们继续做吧。
swift
final class Article: Sendable {
let title: String
var likeCount = 0
init(title: String) {
self.title = title
}
}
在这个阶段,编译器将开始抱怨: ** "Stored property 'likeCount' of 'Sendable'-conforming class 'Article' is mutable"**.
此错误意味着,如果我们希望Article
类型是可发送的,我们就不能拥有可变的存储属性(likeCount
)。 这是因为同时与可变存储属性共享引用类型不安全,会引起潜在数据竞争。
就我们而言,我们绝对不能让likeCount
成为常数,那么我们还有什么其他选择呢? 根据苹果的说法,有许多不同类型的类型是可以发送的:
我们有一个选择是将Article
更改为值类型。
swift
struct Article: Sendable {
let title: String
var likeCount = 0
init(title: String) {
self.title = title
}
}
这一次,Article
结构没有给我们任何编译器错误,但我们在示例代码的其他一些地方确实收到了多个编译器错误。
修复所有这些编译器错误确实需要相当多的代码更改,但这绝对是可行的。 因此,我会把这个作为练习留给你。 如果您确实陷入困境,可随时在这里找到解决方案。
正如您从上面的示例中看到的,遵守Sendable
可发送协议不会自动使一种数据类型可发送。 然而,它确实执行了我们需要遵循的规则,以免意外地为数据竞争创造潜力。 因此,下次您处理Actor时,请确保使任何可共享的可变状态符合Sendable
协议。
期待未来的改善
在WWDC中,苹果提到,未来,Swift编译器将阻止我们共享不可发送的类型。 如果我们尝试访问actor的不可发送状态,我们将收到编译器错误,如下所示:
至于Swift 5.5,此检查仍然不可用,苹果还没有关于何时可用的信息。 当actor内部有可共享的类型时,目前我们能做的最好的事情是注意并始终遵守Sendable
协议,
小结
在我们的ArticleManager
示例中,防止数据竞争的最佳方法肯定是将整个dislike(_:)
函数移动到ArticleManager
中。出于演示目的,我特意使用Sendable
方法,以便您可以更好地了解Sendable
协议如何帮助防止actor 之外的数据竞争。
在撰写本文时,Swift工程团队仍在积极改进Sendable
检查。如果您有任何意见或想法想分享,请务必在Swift论坛上与他们联系。
最后但并非最不重要的是,您可以在这里获得完整的示例代码。
有关iOS开发和Swift的更多文章,请务必在Twitter上关注我并订阅我的每月时事通讯。
感谢您的阅读。👨🏻💻