提问人:meaning-matters 提问时间:1/30/2023 最后编辑:Robmeaning-matters 更新时间:1/31/2023 访问量:615
为什么 Swift 没有在启动的同一线程上恢复异步函数?
Why does Swift not resume an asynchronous function on the same thread it was started?
问:
在“The Swift Programming Language”的并发一章的介绍部分,我读到:
当异步函数恢复时,Swift 不会执行任何操作 保证该函数将在哪个线程上运行。
这让我很惊讶。这似乎很奇怪,例如,与在 pthreads 中等待信号量相比,执行可以跳转线程。
这让我想到了以下问题:
为什么 Swift 不能保证在同一线程上恢复?
是否有任何规则可以恢复线程 确定?
有没有办法影响这种行为,例如确保它在主线程上恢复?
编辑:我对 Swift 并发和上述后续问题的研究是由于发现在主线程上运行的代码(在 SwiftUI 中)正在另一个线程上执行它的块而触发的。Task
答:
它有助于在一些上下文中处理 Swift 并发:Swift 并发试图提供一种更高级别的方法来处理并发代码,并且与您可能已经习惯的线程模型、线程的低级管理、并发原语(锁定、信号量)等不同,因此您不必花费任何时间考虑低级管理。
在 TSPL 的 Actors 部分,在页面的下方引用:
您可以使用任务将程序分解为孤立的并发部分。任务是相互隔离的,这使得它们可以安全地同时运行......
在 Swift Concurrency 中,a 表示可以并发完成的隔离工作,这里的隔离概念非常重要:当代码与周围的上下文隔离时,它可以完成它需要的工作,而不会对外部世界产生影响,或者受到它的影响。这意味着在理想情况下,真正隔离的任务可以随时在任何线程上运行,并根据需要在线程之间交换,而不会对正在完成的工作(或程序的其余部分)产生任何可衡量的影响。Task
正如@Alexander在上面的评论中提到的,如果做得好,这是一个巨大的好处:当工作以这种方式隔离时,任何可用的线程都可以拾取该工作并执行它,让您的流程有机会完成更多工作,而不是等待特定线程可用。
但是:并非所有代码都可以完全隔离,以至于以这种方式运行;在某些时候,一些代码需要与外界交互。在某些情况下,任务需要相互交互才能共同完成工作;在其他方面,如UI工作,任务需要与非并发代码协调才能产生这种效果。Actor 是 Swift Concurrency 提供的用于帮助进行这种协调的工具。
Actor
有助于确保任务在特定上下文中运行,相对于同样需要在该上下文中运行的其他任务进行串行运行。继续上面的引述:
...这使得它们可以安全地同时运行,但有时您需要在任务之间共享一些信息。Actor 允许您在并发代码之间安全地共享信息。
...Actor 一次只允许一个任务访问其可变状态,这使得多个任务中的代码可以安全地与 Actor 的同一实例进行交互。
除了将 s 用作孤立的状态避风港(如该部分的其余部分所示)之外,您还可以创建 s 并使用它们应该在其上下文中运行来显式注释它们的主体。例如,要使用 TSPL 中的示例,可以在以下上下文中运行任务:Actor
Task
Actor
TemperatureLogger
TemperatureLogger
Task { @TemperatureLogger in
// This task is now isolated from all other tasks which run against
// TemperatureLogger. It is guaranteed to run _only_ within the
// context of TemperatureLogger.
}
与以下项竞争也是如此:MainActor
Task { @MainActor in
// This code is isolated to the main actor now, and won't run concurrently
// with any other @MainActor code.
}
这种方法适用于可能需要访问共享状态的任务,并且需要彼此隔离,但是:如果对此进行测试,您可能会注意到针对同一(非主要)参与者运行的多个任务可能仍在多个线程上运行,或者可能在不同的线程上恢复。什么给了?
Task
s 和 s 是 Swift 并发中的高级工具,它们是你作为开发人员最常使用的工具,但让我们来了解一下实现细节:Actor
Task
s 实际上并不是 Swift 并发中工作的低级原语;工作
是。A 表示 a 语句之间的代码,你从不自己写 a;Swift 编译器获取 s 并从中创建 sJob
Task
await
Job
Task
Job
Job
s 本身不是由 s 运行的,而是由Executor
s 运行的,同样,您永远不会自己直接实例化或使用 s。但是,每个执行器都有一个与之关联的执行器,该执行器
实际上运行提交给该参与者的作业Actor
Executor
Actor
这就是调度实际发挥作用的地方。目前,Swift 并发有两个主要的执行器:
所有非 MainActor
actor 当前都使用全局执行器来调度和执行作业,并且使用主执行器来执行相同的操作。MainActor
作为 Swift 并发的用户,这意味着:
- 如果需要一段代码以独占方式在主线程上运行,则可以将其调度在 上,并且将保证它仅在该线程上运行
MainActor
- 如果在任何其他线程上创建任务,它将在全局协作线程池中的一个或多个线程上运行
Actor
- 如果你针对特定的 运行,将为你管理锁和其他并发原语,这样任务就不会同时修改共享状态
Actor
Actor
- 如果你针对特定的 运行,将为你管理锁和其他并发原语,这样任务就不会同时修改共享状态
综上所述,为了回答您的问题:
为什么 Swift 不能保证在同一线程上恢复?
正如上面的评论中提到的——因为:
- 它不应该是必需的(因为任务应该以一种“我们在哪个线程上?”的细节无关紧要的方式进行隔离),并且
- 能够使用任何一个可用的协作线程意味着您可以继续更快地完成所有工作
但是,“主线程”在许多方面都是特殊的,因此,必须仅使用该线程。当你确实需要确保你只在主线程上时,你就使用主要参与者。@MainActor
是否有任何规则可以确定恢复线程?
对于非带注释的任务,唯一的规则是:协作线程池中的第一个可用线程将拾取工作。@MainActor
改变这种行为需要编写和使用你自己的,这还不太可能(尽管有一些计划使这成为可能)。Executor
有没有办法影响这种行为,例如确保它在主线程上恢复?
对于任意线程,不可以 - 您需要提供自己的执行器来控制该低级细节。
但是,对于主线程,您有几个工具:
当您使用
Task.init(priority:operation:)
创建时,它默认为从当前 actor 继承,无论它碰巧是什么 actor。这意味着,如果您已经在主 actor 上运行,则任务将继续使用当前 actor;但如果你不是,它就不会。要显式批注您希望任务在主参与者上运行,您可以显式批注其操作:Task
Task { @MainActor in // ... }
这将确保无论在哪个 actor 上创建,包含的代码都只会在主 actor 上运行。
Task
在 : 中,无论您当前使用哪个 actor,您始终可以使用
MainActor.run(resultType:body:)
将作业直接提交到主 actor 上。闭包已经注解为 ,并将保证在主线程上执行Task
body
@MainActor
请注意,创建分离的任务永远不会从当前执行组件继承,因此可以保证分离的任务将通过全局执行器隐式调度。
我对 Swift 并发和上述后续问题的研究是由于发现从主线程(在 SwiftUI 中)上运行的代码开始的 Task 正在另一个线程上执行它的块而触发的。
在这里查看特定的代码来解释到底发生了什么会有所帮助,但有两种可能性:
- 您创建了一个非显式注释的 ,它恰好在当前线程上开始执行。但是,由于您没有绑定到主要角色,因此它碰巧被其中一个合作线程暂停和恢复
@MainActor
Task
- 你创建了一个包含其他 s 的线程,这些 s 可能在其他 actor 上运行,或者是显式分离的任务 - 并且该工作在另一个线程上继续进行
Task
Task
如需更深入地了解此处的细节,请查看 Swift concurrency: Behind the scenes from WWDC2021,@Rob评论中链接。正在发生的事情的细节还有很多,获得一个更低层次的视图可能会很有趣。
评论
priority
Task
Task
onChange(of:)
@MainActor
Task
.task(priority:_:),
对 SwiftUI 有更直接了解的人可能会更好地插话。该文档似乎没有对给定工作的运行方式做出任何承诺,并且该操作也没有以任何方式进行注释。如果你想安全起见,注释是要走的路。@MainActor
如果您想深入了解 Swift 并发背后的线程模型,请观看 WWDC 2021 视频 Swift 并发:幕后花絮。
回答您的一些问题:
- 为什么 Swift 不能保证在同一线程上恢复?
因为,作为一种优化,在已经在 CPU 内核上运行的某个线程上运行它通常更有效。正如他们在那段视频中所说:
当线程在 Swift 并发下执行工作时,它们在延续之间切换,而不是执行完整的线程上下文切换。这意味着我们现在只需支付函数调用的成本。...
你继续问:
- 是否有任何规则可以确定恢复线程?
除了主要参与者之外,不,无法保证它使用哪个线程。
(顺便说一句,我们已经在这种环境中生活了很长时间。值得注意的是,除了主队列之外,GCD 调度队列也无法保证调度到特定串行队列的两个区块将在同一线程上运行。
- 有没有办法影响这种行为,例如确保它在主线程上恢复?
如果我们需要在主 actor 上运行某些东西,我们只需将该方法隔离到主 actor 上(在闭包、方法或封闭类上指定)。从理论上讲,也可以使用 ,但这通常是解决它的错误方法。@MainActor
MainActor.run {…}
评论
MainActor.run {…}
MainActor.run
评论
@MainActor