[英]Why does Swift not resume an asynchronous function on the same thread it was started?
在“The Swift Programming Language”并发章节的介绍部分,我读到:
当异步 function 恢复时,Swift 不保证 function 将在哪个线程上运行。
这让我很吃惊。 这看起来很奇怪,例如与 pthreads 中等待信号量相比,执行可以跳转线程。
这使我想到以下问题:
为什么 Swift 不保证在同一个线程上恢复?
是否有任何规则可以确定恢复线程?
有没有办法影响这种行为,例如确保它在主线程上恢复?
编辑:我对 Swift 并发性的研究以及上面的后续问题是通过发现从主线程(在 SwiftUI 中)上运行的代码开始的Task
正在另一个线程上执行它的块而触发的。
它有助于在某些上下文中接近 Swift 并发:Swift 并发尝试提供一种更高级别的方法来处理并发代码,并代表您可能已经习惯的线程模型和线程的低级别管理,并发原语(锁定、信号量)等,这样您就不必花任何时间考虑低级管理。
在 TSPL 的Actors 部分,在您引用的页面下方一点:
您可以使用任务将您的程序分解为孤立的、并发的部分。 任务彼此隔离,这使得它们可以安全地同时运行……
在 Swift Concurrency 中, Task
代表可以并发完成的隔离工作,这里隔离的概念非常重要:当代码与其周围的上下文隔离时,它可以完成所需的工作而不会产生影响在外面的世界,或受到它的影响。 这意味着在理想情况下,真正隔离的任务可以随时在任何线程上运行,并根据需要跨线程交换,而不会对正在完成的工作(或程序的 rest)产生任何可衡量的影响。
正如@Alexander 在上面的评论中提到的,如果做得好,这是一个巨大的好处:当以这种方式隔离工作时,任何可用的线程都可以接手该工作并执行它,让您的流程有机会完成更多的工作,而不是等待特定线程可用。
然而:并非所有代码都可以完全隔离以致于以这种方式运行; 在某些时候,一些代码需要与外部世界交互。 在某些情况下,任务需要相互交互才能共同完成工作; 在其他情况下,如 UI 工作,任务需要与非并发代码协调才能产生这种效果。 Actor是 Swift Concurrency 提供的工具来帮助进行这种协调。
Actor
有助于确保任务在特定上下文中运行,相对于其他也需要在该上下文中运行的任务而言是连续的。 继续上面的引用:
…这使它们可以安全地同时运行,但有时您需要在任务之间共享一些信息。 Actor 让您可以在并发代码之间安全地共享信息。
…actor 一次只允许一个任务访问其可变的 state,这使得多个任务中的代码与 actor 的同一个实例交互是安全的。
除了将Actor
用作 state 的隔离避风港(如该部分的 rest 所示)之外,您还可以创建Task
并使用它们应在其上下文中运行的Actor
显式注释它们的主体。 例如,要使用 TSPL 的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.
}
这种方法适用于可能需要访问共享 state 并且需要彼此隔离的任务,但是:如果你对此进行测试,你可能会注意到针对同一个(非主要)actor 运行的多个任务可能仍在运行在多个线程上,或者可以在不同的线程上恢复。 是什么赋予了?
Task
和Actor
是 Swift 并发中的高级工具,它们是您作为开发人员接触最多的工具,但让我们进入实现细节:
Task
实际上不是Swift 并发中 work 的低级原语; Job
是。 Job
表示await
语句之间的Task
中的代码,您永远不会自己编写Job
; Swift 编译器获取Task
并从中创建Job
Job
本身不是由Actor
运行的,而是由Executor
运行的,同样,您永远不会自己直接实例化或使用Executor
。 但是,每个Actor
都有一个与之关联的Executor
,它实际运行提交给该 actor 的作业这是调度实际发挥作用的地方。 目前Swift并发有两个主要执行者:
所有非MainActor
actor 目前都使用全局执行器来调度和执行作业,而MainActor
使用主执行器来做同样的事情。
作为Swift并发的用户,这意味着:
MainActor
上,它会保证只在该线程上运行Actor
上创建任务,它将在全局协作线程池中的一个(或多个)线程上运行
Actor
运行, Actor
将为你管理锁和其他并发原语,这样任务就不会同时修改共享 state有了所有这些,就可以回答您的问题:
为什么 Swift 不保证在同一个线程上恢复?
正如上面评论中提到的——因为:
但是,“主线程”在很多方面都很特殊,因此, @MainActor
只能使用该线程。 当您确实需要确保您独占主线程时,您可以使用主要参与者。
是否有任何规则可以确定恢复线程?
非@MainActor
任务的唯一规则是:协作线程池中的第一个可用线程将接手工作。
更改此行为需要编写和使用您自己的Executor
,这不太可能( 尽管有一些计划使这成为可能)。
有没有办法影响这种行为,例如确保它在主线程上恢复?
对于任意线程,不——您需要提供自己的执行程序来控制底层细节。
但是,对于主线程,您有几个工具:
当你使用Task.init(priority:operation:)
创建一个Task
时,它默认继承当前的 actor,无论这个 actor 是什么。 这意味着如果您已经在主要演员上运行,任务将继续使用当前演员; 但如果你不是,它就不会。 要显式注释您希望任务在主要参与者上运行,您可以显式注释其操作:
Task { @MainActor in //... }
这将确保无论Task
是在哪个 actor 上创建的,包含的代码都只会在主要 actor 上运行。
在Task
中:无论您当前在哪个 actor 上,您始终可以使用MainActor.run(resultType:body:)
将作业直接提交给主要 actor。 body
闭包已经被注释为@MainActor
,并且将保证在主线程上执行
请注意,创建分离任务永远不会从当前参与者继承,因此保证分离任务将通过全局执行程序隐式调度。
我对 Swift 并发性的研究以及上面的后续问题是通过发现从主线程(在 SwiftUI 中)上运行的代码开始的任务正在另一个线程上执行它的块而触发的。
这将有助于在此处查看具体代码以准确解释发生了什么,但有两种可能性:
@MainActor
的Task
,它恰好在当前线程上开始执行。 然而,因为你没有绑定到主要演员,它碰巧被其中一个合作线程暂停和恢复Task
,其中包含其他Task
,这些 Task 可能已经在其他参与者上运行,或者是明确分离的任务——并且该工作在另一个线程上继续要更深入地了解这里的细节,请查看Swift 并发性:WWDC2021 的幕后,@Rob 在评论中链接。 关于正在发生的事情的细节还有很多,获得更底层的视图可能会很有趣。
如果您想深入了解底层 Swift 并发的线程 model,请观看 WWDC 2021 视频Swift 并发:幕后。
在回答你的几个问题时:
- 为什么 Swift 不保证在同一个线程上恢复?
因为,作为一种优化,在某个已经在 CPU 内核上运行的线程上运行它通常会更有效率。 正如他们在该视频中所说:
当线程在 Swift 并发下执行工作时,它们会在延续之间切换,而不是执行完整的线程上下文切换。 这意味着我们现在只需支付 function 呼叫的费用。 ……
你go上去问:
- 是否有任何规则可以确定恢复线程?
除了主要演员,不,不能保证它使用哪个线程。
(顺便说一句,我们已经在这种环境中生活了很长时间。值得注意的是,除了主队列之外,GCD 调度队列无法保证调度到特定串行队列的两个块将在同一队列上运行线程,要么。)
- 有没有办法影响这种行为,例如确保它在主线程上恢复?
如果我们需要在主要参与者上运行某些东西,我们只需将该方法隔离到主要参与者(在闭包、方法或封闭类上使用@MainActor
指定)。 理论上,也可以使用MainActor.run {…}
,但这通常是错误的解决方法。
问题未解决?试试以下方法:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.