为什么 Swift 没有在启动的同一线程上恢复异步函数?

Why does Swift not resume an asynchronous function on the same thread it was started?

提问人:meaning-matters 提问时间:1/30/2023 最后编辑:Robmeaning-matters 更新时间:1/31/2023 访问量:615

问:

“The Swift Programming Language”的并发一章的介绍部分,我读到:

当异步函数恢复时,Swift 不会执行任何操作 保证该函数将在哪个线程上运行。

这让我很惊讶。这似乎很奇怪,例如,与在 pthreads 中等待信号量相比,执行可以跳转线程。

这让我想到了以下问题:

  • 为什么 Swift 不能保证在同一线程上恢复?

  • 是否有任何规则可以恢复线程 确定?

  • 有没有办法影响这种行为,例如确保它在主线程上恢复?

编辑:我对 Swift 并发和上述后续问题的研究是由于发现在线程上运行的代码(在 SwiftUI 中)正在另一个线程上执行它的块而触发的。Task

iOS Swift 异步 swift 并发

评论

7赞 Alexander 1/30/2023
反过来想:为什么要在同一线程上恢复?假设你有一个已挂起的函数,该函数已准备好恢复,但上次运行它的线程尚不可用。如果协作线程池中的另一个线程释放了,它应该接受它,还是继续等待并坚持其原始线程?如您所见,提供这种保证并不是免费的,因此您必须权衡它和收益。知道它将在同一线程上恢复有什么好处?
2赞 Itai Ferber 1/30/2023
“更容易理解”有点主观——你能详细说明一下你的想法吗?补充一点@Alexander可能想要表达的内容:Swift Concurrency 中的一个重要概念是代码应该与运行它的线程隔离,这样你就不需要思考或检查“我现在在哪个线程上?”,就像你描述内核一样:“运行一段代码的线程永远不应该触及程序员理解的区域/水平代码]。
4赞 Itai Ferber 1/30/2023
如果你指的是主线程与非主线程行为,有一些工具专门用于处理这个问题,我们可以详细说明这一点——问题是,你是否关心代码是在线程 5 上运行还是在线程 7 上运行,如果是,为什么?如果你只是在考虑“主线程还是非主线程?”,那么答案很容易给出。
2赞 Itai Ferber 1/30/2023
具体来说,任务可以在 上运行,保证只在主线程上运行。这使得使用 UI 更新变得微不足道。@MainActor
2赞 Itai Ferber 1/30/2023
伟大!那么,这有助于定制一个答案。现在正在做一个。

答:

11赞 Itai Ferber 1/31/2023 #1

它有助于在一些上下文中处理 Swift 并发:Swift 并发试图提供一种更高级别的方法来处理并发代码,并且与您可能已经习惯的线程模型、线程的低级管理、并发原语(锁定、信号量)等不同,因此您不必花费任何时间考虑低级管理。

在 TSPL 的 Actors 部分,在页面的下方引用:

您可以使用任务将程序分解为孤立的并发部分。任务是相互隔离的,这使得它们可以安全地同时运行......

在 Swift Concurrency 中,a 表示可以并发完成的隔离工作,这里的隔离概念非常重要:当代码与周围的上下文隔离时,它可以完成它需要的工作,而不会对外部世界产生影响,或者受到它的影响。这意味着在理想情况下,真正隔离的任务可以随时在任何线程上运行,并根据需要在线程之间交换,而不会对正在完成的工作(或程序的其余部分)产生任何可衡量的影响。Task

正如@Alexander在上面的评论中提到的,如果做得好,这是一个巨大的好处:当工作以这种方式隔离时,任何可用的线程都可以拾取该工作并执行它,让您的流程有机会完成更多工作,而不是等待特定线程可用。

但是:并非所有代码都可以完全隔离,以至于以这种方式运行;在某些时候,一些代码需要与外界交互。在某些情况下,任务需要相互交互才能共同完成工作;在其他方面,如UI工作,任务需要与非并发代码协调才能产生这种效果。Actor 是 Swift Concurrency 提供的用于帮助进行这种协调的工具。

Actor有助于确保任务在特定上下文中运行,相对于同样需要在该上下文中运行的其他任务进行串行运行。继续上面的引述:

...这使得它们可以安全地同时运行,但有时您需要在任务之间共享一些信息。Actor 允许您在并发代码之间安全地共享信息。

...Actor 一次只允许一个任务访问其可变状态,这使得多个任务中的代码可以安全地与 Actor 的同一实例进行交互。

除了将 s 用作孤立的状态避风港(如该部分的其余部分所示)之外,您还可以创建 s 并使用它们应该在其上下文中运行来显式注释它们的主体。例如,要使用 TSPL 中的示例,可以在以下上下文中运行任务:ActorTaskActorTemperatureLoggerTemperatureLogger

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.
}

这种方法适用于可能需要访问共享状态的任务,并且需要彼此隔离,但是:如果对此进行测试,您可能会注意到针对同一(非主要)参与者运行的多个任务可能仍在多个线程上运行,或者可能在不同的线程上恢复。什么给了?


Tasks 和 s 是 Swift 并发中的高级工具,它们是你作为开发人员最常使用的工具,但让我们来了解一下实现细节:Actor

  1. Tasks 实际上并不是 Swift 并发中工作的低级原语;工作是。A 表示 a 语句之间的代码,你从不自己写 a;Swift 编译器获取 s 并从中创建 sJobTaskawaitJobTaskJob
  2. Jobs 本身不是由 s 运行的,而是由 Executors 运行的,同样,您永远不会自己直接实例化或使用 s。但是,每个执行器都有一个与之关联的执行器,该执行器实际上运行提交给该参与者的作业ActorExecutorActor

这就是调度实际发挥作用的地方。目前,Swift 并发有两个主要的执行器:

  1. 协作的全局执行程序,用于在协作线程池上调度作业,以及
  2. 执行器,以独占方式在主线程上调度作业

所有非 MainActor actor 当前都使用全局执行器来调度和执行作业,并且使用主执行器来执行相同的操作。MainActor

作为 Swift 并发的用户,这意味着:

  1. 如果需要一段代码以独占方式在主线程上运行,则可以将其调度在 上,并且将保证它仅在该线程上运行MainActor
  2. 如果在任何其他线程上创建任务,它将在全局协作线程池中的一个或多个线程上运行Actor
    • 如果你针对特定的 运行,将为你管理锁和其他并发原语,这样任务就不会同时修改共享状态ActorActor

综上所述,为了回答您的问题:

为什么 Swift 不能保证在同一线程上恢复?

正如上面的评论中提到的——因为:

  1. 它不应该是必需的(因为任务应该以一种“我们在哪个线程上?”的细节无关紧要的方式进行隔离),并且
  2. 能够使用任何一个可用的协作线程意味着您可以继续更快地完成所有工作

但是,“主线程”在许多方面都是特殊的,因此,必须仅使用该线程。当你确实需要确保你只在主线程上时,你就使用主要参与者。@MainActor

是否有任何规则可以确定恢复线程?

对于非带注释的任务,唯一的规则是:协作线程池中的第一个可用线程将拾取工作。@MainActor

改变这种行为需要编写和使用你自己的,这还不太可能(尽管有一些计划使这成为可能)。Executor

有没有办法影响这种行为,例如确保它在主线程上恢复?

对于任意线程,不可以 - 您需要提供自己的执行器来控制该低级细节。

但是,对于线程,您有几个工具:

  1. 当您使用 Task.init(priority:operation:) 创建时,它默认为从当前 actor 继承,无论它碰巧是什么 actor。这意味着,如果您已经在主 actor 上运行,则任务将继续使用当前 actor;但如果你不是,它就不会。要显式批注您希望任务在主参与者上运行,您可以显式批注其操作:Task

    Task { @MainActor in
        // ...
    }
    

    这将确保无论在哪个 actor 上创建,包含的代码都只会在主 actor 上运行。Task

  2. 在 : 中,无论您当前使用哪个 actor,您始终可以使用 MainActor.run(resultType:body:) 将作业直接提交到主 actor 上。闭包已经注解为 ,并将保证在主线程上执行Taskbody@MainActor

请注意,创建分离的任务永远不会从当前执行组件继承,因此可以保证分离的任务将通过全局执行器隐式调度。

我对 Swift 并发和上述后续问题的研究是由于发现从主线程(在 SwiftUI 中)上运行的代码开始的 Task 正在另一个线程上执行它的块而触发的。

在这里查看特定的代码来解释到底发生了什么会有所帮助,但有两种可能性:

  1. 您创建了一个非显式注释的 ,它恰好在当前线程上开始执行。但是,由于您没有绑定到主要角色,因此它碰巧被其中一个合作线程暂停和恢复@MainActorTask
  2. 你创建了一个包含其他 s 的线程,这些 s 可能在其他 actor 上运行,或者是显式分离的任务 - 并且该工作在另一个线程上继续进行TaskTask

如需更深入地了解此处的细节,请查看 Swift concurrency: Behind the scenes from WWDC2021,@Rob评论中链接。正在发生的事情的细节还有很多,获得一个更低层次的视图可能会很有趣。

评论

2赞 Alexander 1/31/2023
哇,这是一个镜面答案。关于工作的 TIL。感谢您抽出宝贵时间写下这篇文章!
1赞 Itai Ferber 1/31/2023
@Rob 这是一个很好的观点——我太草率了。我所说的“全局执行者”是指“全局执行者”,而“全局线程”是指属于全局执行者的协作线程池。我更新了答案,以更准确地使用“全球”一词。
1赞 Itai Ferber 1/31/2023
设置任务的目的实际上是为了帮助调度程序更好地决定接下来要排队哪些工作:当有多个任务可供运行时,调度程序将优先处理优先级较高的任务,而不是优先级较低的任务。priority
1赞 Itai Ferber 1/31/2023
是的,使用答案中提到的初始值设定项,您将始终在运行时继承当前处于活动状态的 actor,这意味着在保证在主线程上运行的函数内部生成应始终继承 main actor。(假设 SwiftUI 保证了这一点,它应该是安全的。但是:显式注释代码并没有什么坏处——如果你随着时间的推移重构代码,并最终从其他地方创建代码,你可能不再在主线程上运行!TaskTaskonChange(of:)@MainActorTask
1赞 Itai Ferber 1/31/2023
至于 SwiftUI 的 .task(priority:_:),对 SwiftUI 有更直接了解的人可能会更好地插话。该文档似乎没有对给定工作的运行方式做出任何承诺,并且该操作也没有以任何方式进行注释。如果你想安全起见,注释是要走的路。@MainActor
5赞 Rob 1/31/2023 #2

如果您想深入了解 Swift 并发背后的线程模型,请观看 WWDC 2021 视频 Swift 并发:幕后花絮

回答您的一些问题:

  1. 为什么 Swift 不能保证在同一线程上恢复?

因为,作为一种优化,在已经在 CPU 内核上运行的某个线程上运行它通常更有效。正如他们在那段视频中所说

当线程在 Swift 并发下执行工作时,它们在延续之间切换,而不是执行完整的线程上下文切换。这意味着我们现在只需支付函数调用的成本。...

你继续问:

  1. 是否有任何规则可以确定恢复线程?

除了主要参与者之外,不,无法保证它使用哪个线程。

(顺便说一句,我们已经在这种环境中生活了很长时间。值得注意的是,除了主队列之外,GCD 调度队列也无法保证调度到特定串行队列的两个区块将在同一线程上运行。

  1. 有没有办法影响这种行为,例如确保它在主线程上恢复?

如果我们需要在主 actor 上运行某些东西,我们只需将该方法隔离到主 actor 上(在闭包、方法或封闭类上指定)。从理论上讲,也可以使用 ,但这通常是解决它的错误方法。@MainActorMainActor.run {…}

评论

0赞 meaning-matters 1/31/2023
为什么你认为通常是错误的方式?MainActor.run {…}
1赞 Rob 1/31/2023
因为在 Swift 并发中,其中一个激励性的想法是摆脱脆弱的 GCD 模式,在这种模式中,调用者经常必须知道对某个方法的调用需要调度到哪个队列。取而代之的是,我们现在只需标记某个例程被隔离到哪个 actor,当我们从异步上下文中调用它时,编译器将为我们处理它。例如,请参阅 WWDC 2021 视频 Swift 并发:更新示例应用,他们首先建议,但后来显示为什么不需要它,以及为什么。MainActor.run
0赞 meaning-matters 1/31/2023
谢谢Rob,我会看那个视频。您是否知道任何深入解释 Swift 并发的可读来源(因为我对 TSPL 感到失望,而且视频不适合以后参考)?
1赞 Rob 2/1/2023
不,我没有。我个人非常依赖这些视频(并且成绩单是可搜索的),尽管有您的批评。Swift Evolution 提案提供了有关基本原理和实现的见解。网上有大量用户生成的文章,但可能有点喜忧参半。