ios 为什么Swift不能在它启动的线程上恢复异步函数?

svmlkihl  于 2023-02-01  发布在  iOS
关注(0)|答案(2)|浏览(103)

"The Swift Programming Language"的并发一章的介绍部分我读到:
当一个异步函数恢复时,Swift并不保证该函数将在哪个线程上运行。
这让我很吃惊,例如,与pthreads中等待信号量相比,执行可以跳转线程,这似乎很奇怪。
这使我提出以下问题:

  • 为什么Swift不能保证在同一线程上继续?
  • 是否有任何规则可以用来确定恢复线程?
  • 是否有办法影响这种行为,例如确保它在主线程上继续?

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

lawou6xi

lawou6xi1#

它有助于在某些背景下实现Swift并发:Swift并发试图提供一种处理并发代码的"更高级别"方法,并且代表了与您可能已经习惯的线程模型、线程的低级管理、并发原语(锁定、信号量)等的不同,因此您不必花任何时间考虑低级管理。
从TSPL的演员部分,在你的报价页面上再往下一点:
你可以用任务把你的程序分解成独立的、并发的部分。任务是相互隔离的,这就是为什么它们可以安全地同时运行。
在Swift Concurrency中,Task表示可以并发完成的一个 * 隔离 * 工作位,这里的 * 隔离 * 概念非常重要:当代码与它周围的环境隔离时,它可以做它需要做的工作,而不会对外部世界产生影响,也不会受到外部世界的影响。2这意味着在理想的情况下,一个真正隔离的任务可以在任何时候在任何线程上运行,并根据需要在线程之间交换,而不会对正在做的工作(或程序的其余部分)产生任何可测量的影响。
正如@Alexander在上面的评论中提到的,这是一个巨大的好处,如果做得好的话:当工作以这种方式被隔离时,任何可用的线程都可以拾取该工作并执行它,从而使您的进程有机会完成更多的工作,而不是等待特定线程可用。

  • 然而 *:并不是所有的代码都能如此完全地隔离到以这种方式运行;在某些情况下,一些代码需要与外部世界接口。2在某些情况下,任务需要彼此接口以共同完成工作;在其他情况下(如UI工作),任务需要与非并发代码协调才能实现这种效果。Actors是Swift Concurrency提供工具,用于帮助实现这种协调。

Actor的帮助确保任务在特定的 * 上下文 * 中运行,相对于也需要在该上下文中运行的其他任务连续运行。
...这使得它们可以安全地同时运行,但有时您需要在任务之间共享一些信息。使用者可以在并发代码之间安全地共享信息。
...参与者一次只允许一个任务访问其可变状态,这使得多个任务中的代码可以安全地与参与者的同一示例交互。
除了使用Actor作为状态的隔离避风港(如该部分的其余部分所示)之外,您还可以创建Task,并使用Actor显式注解其主体,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.
}

这种方法对于那些需要访问共享状态,并且需要彼此隔离的任务来说效果很好,**但是:**如果您对此进行测试,您可能会注意到针对同一个(非主)参与者运行的多个任务可能仍然在多个线程上运行,或者可能在不同的线程上继续运行。
Task s和Actor s是Swift并发中的 * 高级 * 工具,也是开发人员最常接触的工具,但让我们来了解一下实现细节:

  1. Task实际上 * 不是 * Swift并发中工作的低级原语;Job表示Taskawait语句之间的代码,并且您从未自己编写过Job;Swift编译器接受Task并从中创建Job
  2. Job本身不是由Actor运行的,而是由Executor运行的,同样,您自己永远不会示例化或直接使用Executor。但是,每个Actor * 都有 * an Executor associated with it,它实际运行提交给该执行元的作业
    这就是调度真正发挥作用的地方。目前Swift并发中有两个主要执行器:
    1.一个协作global executor,它在cooperative thread pool上调度作业,以及
    1.一个 * main * 执行器,专门在主线程上调度作业
    所有非MainActor参与者目前都使用全局执行器来调度和执行作业,MainActor使用主执行器来做同样的事情。
    作为Swift并发的"用户",这意味着:
    1.如果您 * 需要 * 一段代码在主线程上独占运行,您可以在MainActor上调度它,这样就可以保证它 * 只 * 在该线程上运行
  • 如果您在任何其他Actor上创建任务,它将在全局协作线程池中的一个(或多个)线程上运行
  • 如果您针对 * 特定 * Actor运行,Actor将为您管理锁和其他并发原语,以便任务不会并发地修改共享状态

所有这些,为了回答你们的问题:
为什么Swift不能保证在同一线程上继续?
如上文评论中所述-因为:

1.这应该是不必要的(因为任务应该以一种"我们在哪个线程上"的细节无关紧要的方式隔离),以及
1.能够使用任何一个可用的协作线程意味着您可以继续以更快的速度完成所有工作
然而,"主线程"在很多方面都是特殊的,因此@MainActor只能使用这个线程,当你确实需要确保你独占主线程时,你可以使用主参与者。
是否有任何规则可以用来确定恢复线程?
非-@MainActor注解任务的唯一规则是:协作线程池中的第一个可用线程将接管该工作。
改变这种行为需要编写和使用您自己的Executor,这还不太可能(though there are some plans on making this possible)。
是否有办法影响这种行为,例如确保它在主线程上继续?
对于 * 任意 * 线程,不需要-您需要提供自己的执行器来控制低级别细节。
但是,对于 * main * 线程,您有几个工具:
1.当你使用Task.init(priority:operation:)创建一个Task时,它默认从 * current * actor继承,不管这个actor是什么,这意味着如果你已经在主actor上运行,任务将继续使用当前actor;但是如果你不是,它就不会运行。要显式地注解你希望任务在主要参与者上运行,你可以显式地注解它的操作:

Task { @MainActor in
    // ...
}

这将确保无论Task是在哪个参与者上创建的,所包含的代码都将只在主参与者上运行。
1.从一个Task * 之内 *:不管您当前在哪个参与者上,您总是可以使用MainActor.run(resultType:body:)将作业直接提交到主参与者上。body闭包已经注解为@MainActor,并且将保证在主线程上执行
注意,创建detached task将 * 永远 * 不会继承当前actor,因此可以保证分离的任务将通过全局执行器隐式调度。
我对Swift并发性的研究和上面的后续问题是由于发现从主线程(SwiftUI中)上运行的代码开始的任务正在另一个线程上执行它的块而引发的。
在这里查看具体的代码来解释发生了什么会有所帮助,但有两种可能性:
1.您创建了一个非显式注解@MainActorTask,并且它碰巧在当前线程上开始执行,但是,因为您没有 * 绑定 * 到主要参与者,所以它碰巧被一个协作线程挂起并恢复
1.您创建了一个Task,其中包含其他Task,这些Task可能已经在其他参与者上运行,或者是显式分离的任务-并且该工作在另一个线程上继续
想了解更多细节,请查看@Rob在评论中链接的WWDC2021的Swift concurrency: Behind the scenes,其中有很多关于正在发生的事情的细节,如果能得到一个更低层次的视图,可能会很有趣。

aiazj4mn

aiazj4mn2#

如果您想深入了解Swift并发底层的线程模型,请观看WWDC 2021视频Swift concurrency: Behind the scenes
回答你的几个问题:
1.为什么Swift不能保证在同一线程上继续?
因为,作为一种优化,在已经在CPU内核上运行的某个线程上运行它通常会更高效。As they say in that video
当线程在Swift并发下执行工作时,它们会在continuation之间切换,而不是执行完整的线程上下文切换。这意味着我们现在只需支付函数调用的成本。
你接着问:
1.是否有任何规则可以用来确定恢复线程?
除了主要参与者,不,没有关于它使用哪个线程的保证。
(As另外,我们已经在这种环境中生活了很长时间,值得注意的是,除了主队列之外,GCD分派队列也不能保证分派到特定串行队列的两个块将在同一个线程上运行。)
1.是否有办法影响这种行为,例如确保它在主线程上继续?
如果我们需要在主参与者上运行一些东西,我们只需将该方法与主参与者隔离(在闭包、方法或封闭类上使用@MainActor指定)。理论上,也可以使用MainActor.run {…},但这通常是错误的处理方式。

相关问题