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

mea*_*ers 5 asynchronous ios swift swift-concurrency

在《Swift 编程语言》并发章节的介绍部分中, 我读到:

\n
\n

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

\n
\n

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

\n

这让我产生以下问题:

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

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

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

    \n
  • \n
\n

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

\n

Ita*_*ber 12

它有助于在某些上下文中处理 Swift 并发:Swift 并发尝试提供一种更高级别的方法来处理并发代码,并且代表了与您可能已经习惯的线程模型和线程的低级别管理的背离,并发原语(锁定、信号量)等,这样您就不必花时间考虑底层管理。

\n

来自TSPL 的演员部分,在您引用的页面的更下方:

\n
\n

您可以使用任务将程序分解为独立的并发部分。任务彼此隔离,这使得它们可以安全地同时运行\xe2\x80\xa6

\n
\n

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

\n

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

\n

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

\n

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

\n
\n

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

\n

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

\n
\n

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

\n
Task { @TemperatureLogger in\n    // This task is now isolated from all other tasks which run against\n    // TemperatureLogger. It is guaranteed to run _only_ within the\n    // context of TemperatureLogger.\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这同样适用于运行MainActor

\n
Task { @MainActor in\n    // This code is isolated to the main actor now, and won\'t run concurrently\n    // with any other @MainActor code.\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
\n

Tasks 和Actors 是Swift 并发中的高级工具,它们是您作为开发人员使用最多的工具,但让我们深入了解实现细节:

\n
    \n
  1. Task实际上,s 并不是Swift并发中工作的低级原语;Job是。A代表a Between语句Job中的代码,永远不要自己写a;Swift 编译器获取s 并从中创建 sTaskawaitJobTaskJob
  2. \n
  3. Jobs 本身不是由Actors 运行,而是由Executors 运行,同样,您永远不会Executor亲自实例化或直接使用 s 。然而,每个Actor 都有 一个Executor与之关联的,它实际上运行提交给该参与者的作业
  4. \n
\n

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

\n
    \n
  1. 一个协作的全局执行器,它在协作线程池上调度作业,以及
  2. \n
  3. 执行器,专门在主线程上调度作业
  4. \n
\n

目前,所有非MainActor参与者都使用全局执行器来调度和执行作业,并MainActor使用主执行器来执行相同的操作。

\n

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

\n
    \n
  1. 如果你需要一段代码专门在主线程上运行,你可以将它安排在 上MainActor,并且它将保证在该线程上运行
  2. \n
  3. 如果您在任何其他 上创建任务Actor,它将在全局协作线程池中的一个(或多个)线程上运行\n
      \n
    • 如果您针对特定的 Actor运行,它将Actor为您管理锁和其他并发原语,以便任务不会同时修改共享状态
    • \n
    \n
  4. \n
\n
\n

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

\n
\n

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

\n
\n

正如上面评论中提到的 \xe2\x80\x94 因为:

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

然而,“主线程”在很多方面都是特殊的,因此,必须@MainActor仅使用该线程。当您确实需要确保您独占主线程时,您可以使用主要参与者。

\n
\n

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

\n
\n

非注释任务的唯一规则@MainActor是:协作线程池中的第一个可用线程将接管工作。

\n

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

\n
\n

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

\n
\n

对于任意线程,没有 \xe2\x80\x94 您需要提供自己的执行器来控制低级细节。

\n

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

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

    \n
    Task { @MainActor in\n    // ...\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

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

    \n
  2. \n
  3. 从aTask:无论您当前在哪个演员中,您始终可以使用 直接向主要演员提交作业MainActor.run(resultType:body:)。闭body包已被注释为@MainActor,并将保证在主线程上执行

    \n
  4. \n
\n

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

\n
\n

我对 Swift 并发性和上述后续问题的研究是通过发现从主线程(在 SwiftUI 中)上运行的代码启动的任务正在另一个线程上执行它的块而触发的。

\n
\n

在这里查看具体代码将有助于准确解释发生了什么,但有两种可能性:

\n
    \n
  1. 您创建了一个非显式@MainActor注释的Task,并且它碰巧开始在当前线程上执行。但是,因为您没有绑定到主要参与者,所以它碰巧被协作线程之一挂起并恢复
  2. \n
  3. 您创建了一个其中Task包含其他Tasks 的文件,这些文件可能已在其他参与者上运行,或者是显式分离的任务 \xe2\x80\x94 并且该工作在另一个线程上继续
  4. \n
\n
\n

要更深入地了解这里的具体细节,请查看Swift 并发:WWDC2021 的幕后故事,@Rob 在评论中链接了该内容。正在发生的事情还有很多细节,获得更低级别的视图可能会很有趣。

\n

  • 哇,这是一个镜面答案。直到关于工作。感谢您花时间写这篇文章! (2认同)

Rob*_*Rob 5

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

\n

回答你的几个问题:

\n
\n
    \n
  1. 为什么 Swift 不保证在同一个线程上恢复?
  2. \n
\n
\n

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

\n
\n

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

\n
\n

你继续问:

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

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

\n

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

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

如果我们需要在主要参与者上运行某些内容,我们只需将该方法隔离到主要参与者(@MainActor在闭包、方法或封闭类上指定)。理论上,也可以使用MainActor.run {\xe2\x80\xa6},但这通常是错误的解决方法。

\n