Swift演员:一个实际的例子,第1部分

2021-05-11 16:52:38

我一直在重新阅读Swift结构化并发路线图和Swift演员提案,并注意到后者的一份说明:

所以,自然🤓,我从swift.org下载了最新的快照,并将其旋转旋转以尝试一些演员代码!

巨大的免责声明:这是使用未切割成释放的中继码的所有实验经验。

我抓住了Swift Toolchain的5月第4个中继快照,在我的机器上安装它,并通过Xcode / Toolchains中将其激活在我的Xcode 12.5 Beta中...菜单。现在我有Xcode运行,swift 5.5💪🏼:

接下来,我需要一个具有启用的实验并发支持的项目,因此我从命令行创建了一个应用程序:

然后我清理了包裹.swift文件并添加了演员提议中提到的Swift编译器标志:

// swift-tools-version:5.4导入packageedescription让包=包裹(名称:" actortest",平台:[.macos(.v11)],targets:[.executableTarget(名称:" Scatortest",SwiftSettings:[.Unsafeflags([" -Xfrontend"" -Enable-Deparical-mancurrency"])])])]))

⌘+ b检查项目是否罚款,这一切都是 - Actorstest现在是一个应用运动实验迅速并发与演员!

我已经读过几次各种快速并发努力提案,我会尽力总结我对演员解决的理解(到目前为止)。

注意:我可能是错误的一些点,或者在以后可能会发生变化。

人是线程安全的吗?换句话说,你的应用程序会 - >永远< - 里面的崩溃?

答案是:“这取决于”。这取决于你或他人的方式如何'代码使用人员。如果两条代码,同时运行,请同时更新person.name - 您的应用程序将崩溃。 🌋

即使您制作人物。私人私有,为了从类外部保护它,您仍然可以在类内同时读/写,该类也将崩溃您的应用。

目前,如果您正在编写并发代码,您可能正在使用Swift的线程Sunitizer来检测像上面我谈论的那样的数据比赛。

这是非常有用的,但问题是这种方式,您仍然可以编写不安全的代码。崩溃可能仍然发生在运行时。您只希望使用Thread Sacitizer发现并修复所有问题。 (并且稍后不会引入新问题。)

Actors以一种类型的数据隔离提供,以便您编写的代码无法创建数据比赛。换句话说,线程安全内置于类型系统中。类似于可变性如何 - 您根本无法编译突变不可变数据的代码。

让我们比较课程与演员。下面的课程不是线程安全,是否崩溃或不依赖于其他代码使用它的方式:

相比之下,以下演员类型可以安全使用。由于工作中的工作方式,您的代码可以安全地访问名称 - 属性内存。它由演员类型保证:

我还没有登录到实施细节,但到目前为止我在提案中读到的内容 - 演员透明地实施以下编译时间规则:

仅允许对来自任何异步上下文的Actoror的成员的异步访问,

这样,演员本身同步访问其数据,但是外部的任何其他代码都是必需的异步访问(具有隐式同步)以防止数据比赛。

返回我的Actortest App,我将首先尝试一些并发代码,在我添加演员以解决崩溃之前崩溃。

我将首先计算Sha512哈希数的缓存类型(一个只需要几行SWIFT的愚蠢示例来演示这个想法):

导入cryptokit类hashcache {private(set)var hasish = [int:string]()func addhash(for number:int){let string = sha512.hash(数据:数据(字符串(字符串).utf8)).description散列[number] = string} func compute(){dissatchqueue.concurrentperform(迭代:15000,执行:AddHash(for :))}}

AddHash(for :)采取一个数字,并将其文本表示的哈希添加到哈希和

如果您之前已经完成了这些行,则立即查看此代码中的问题。当您调用hashcache.compute()时,您将崩溃,因为多个线程正在调用AddHash(for :)并尝试同时变异哈希。 💥

所以再次,哈希和地址(for :)并不是每sè不安全 - 特别是如果您的应用程序通常在主线程上运行。但是一旦一些代码调用AddHash(for :)来自非主线程,该应用程序变为易于数据竞争。

目前,要解决此问题,您将在可变状态下添加锁定或使用串行队列来同步数据访问,但通常需要大量的样板代码。 🙃

让我们将Hashcache重写为演员并摆脱该崩溃。

所以我只是要用演员和...替换关键字类。

导入Cryptokitactor Hashcache {private(set)var hasish = [int:string]()func addhash(for number:int){let string = sha512.hash(数据:数据(字符串(字符串).utf8)).description hashes [ number] = string} func compute(){dissatchqueue.concurrentperform(迭代:15000,执行:AddHash(for :))}}

它有效 - 项目编译得很好。运行该应用程序虽然同样崩溃了。不要忘记此功能是正在进行的工作。 🤷🏽♂️

它似乎暂时编译器不完全捕获dispatchqueue.concurrentperform(...)的执行参数。

但如果您提供关闭,编译器会捕获数据竞争代码!将compute()更改为:

ConcurrentPerform(...)中的闭合被异步调用,因此您不允许同步地访问actor构件,因为需要同步以防止数据比赛。惊人的!

我不会尝试使用DispatchQueue进行代码工作,因为此时,尝试基于任务(以及正在进行的工作)并发等等,它会更有乐趣。让我们用任务组替换DispatchQueue代码:

这是我在上面提到的actor提案的更多示例代码,但它看起来它只是创建了一个可以添加任务的并发组。一个简洁的细节是,使用托斯群(...)等待,直到组中的所有任务完成。

我检查了一种创建不返回任何东西的任务,但无法找到一个的方法,因此我在这个小组中的任务将返回一个我会丢弃的BOOL。

由于哈顿群组(...)需要等待所有任务来完成它,当然是异步。 Swift抱怨我呼叫的上下文(......)不是异步,所以让我们修复:

我制作了compute()async,并且在调用withtaskgroup时使用等待等待(...)。清除之前的错误消息,但我得到一个新的错误:

如果您⌘+点击使用托斯群(...),您会发现实验功能具有遥远的未来可用性:

在任何情况下,单击第三个建议的解决方案使HashCache在MacOS 9999或更新中提供,并且一切都可以编译确定。

接下来,我想为每个即将计算的哈希创建任务。我发现这个可以工作,看起来很简单(再次,我真的没有使用返回的布尔值)。

func compute()async {await withtaskgroup(:bool。self){group for number in 0 ... 15_000 {group.spawn {await self.addhash(for:number)return true}}}

读取组API听起来真的很棒 - 您可以将其视为懒惰的序列并控制任务执行到批处理任务,取消一个或多个任务,作为任务完整的进度发出进度,更多。

但是,我现在不会进入所有这些,因为我现在想要的只是同时计算哈希和完成。

如果滚动waaay向上,您会注意到AddHash(for :)根本不是异步功能。但是从异步任务同步调用AddHash(for :)可能会导致数据竞争和崩溃。这就是编译器要求您使用等待的原因,以便它可以同步您对Actor内部的访问:

这就是我所说的。我们在开始时拼写出的3个简单规则是如此聪明,只需阻止您编写不良代码。

但是,没有必要将AddHash(for :)转换为Async函数。当上下文允许时,您仍然可能希望同步调用它。

现在,我非常好奇,尝试如何运作。让我们将一个哈希添加到Compute()缓存的批次:

func compute()async {addhash(for:42)等待用托斯群(以下是:bool。self){group in 0 ... 15_000 {group.spawn {await self.addhash(for:number)return true}} }}

我没有详细阅读,这使得任务闭合使演员隔离错误的注释是什么,但它在我的Todo上。

验证是否必须使用从演员的外部访问演员成员,而是无论是否来自异步上下文,都必须执行。

AddHash()是一个“香草”功能,但是对于所有目的,它在演员外部调用时会像异步一样处理,最后

在Actor外面访问时,哈希也需要同步,因此您还需要在此处使用等待。

演员实施设计和写作此代码的体验让我非常开心。在编译时间检查的线程安全将使SWIFT应用程序一般更安全,越来越容易发生,希望更多的人将拥抱并发编程。

导入基础导入cryptokit @可用(macos 9999,*)@ main struct app {static let cache = hashcache()静态func main()async {await cache.addhash(for:7778)等待cache.compute()等待打印(缓存.hashes [34]!)}} @可用(macos 9999,*)演员hashcache {private(set)var hasish = [int:string]()func addhash(for number:int){let string = sha512.hash(数据:数据(字符串(数字).utf8)).description hashes [number] = string} func compute()async {addhash(for:42)等待用托斯群(以下是:self){group in for of no.0。 ..15_000 {group.spawn {await self.addhash(for:number)return true}}}}}

最终免责声明:使用Swift.org Trunk Branch的Swift Toolchain开发。并发功能是一个过程。此代码可能在以后的时刻无法正常工作。

更新:在包装此帖后,我开始考虑混合迅速的演员并结合。期待几天后出来的后续帖子。

SWIFT中的所有并发功能都在进行中。您现在不需要依赖于现在,因为语法或行为可能会改变,所以我希望这篇文章对你没有任何不必要的压力。

这也是为什么我在帖子中放置了这么多免责声明的原因 - 这篇文章纯粹是探索性的。

但下次有人告诉你并发代码不好,因为它崩溃了奇怪的错误,最好的方法是只使用主线程,如果您在SWIFT中使用新的结构化并发时,您将知道这不是真的:)

要了解所有组合检查组合:Swift的异步编程 - 这是您可以看到所有更新的地方,在网站论坛中讨论。