Swift并发 – 将函数运行在特定线程上

huangapple go评论77阅读模式
英文:

Swift concurrency - having functions run on specific threads

问题

  1. 关于 async 函数:如果我们想要在主线程和其他线程上运行相同的异步函数,不能像使用 DispatchQueue 那样轻松实现。您需要声明两个不同的异步函数,并将一个标记为 @MainActor,以在代码级别显示事物将在哪里运行。

  2. 关于 Task:如果 Task 不在主 actor 上运行,就没有办法安排单独的 Task 以串行方式运行;相反,您需要分别将每个任务设置为异步函数,然后一个接一个地等待它们。换句话说,没有像 Kotlin 协程那样的串行调度程序,可以将作业安排到其中。

这两个陈述是否正确?Swift 的并发范式与我所预期的大不相同,所以我有些难以理解。

英文:

I have two questions to clarify what I understood is correct:

  1. Regarding async functions: If we wanted to run the same async function sometimes on the main thread and at other times on other threads, you cannot do so like you would with DispatchQueues. You would have to declare two separate async functions and have one annotated @MainActor, showing at the code level where things will be run.

  2. Regarding Tasks: If Tasks are not run on the main actor, there is no way to schedule separate Tasks to run in a serial manner; instead, you would have to make each task an async function respectively and await one after another. In other words, there is no serial dispatcher to which you can schedule jobs like Kotlin coroutine.

Are these two statements correct?

The Swift concurrency paradigm is vastly different from what I was expecting, so I'm having some difficulty wrapping my head around it.

答案1

得分: 2

简而言之,在使用Swift并发性时,不需要过多关注GCD模式的直接类比。通常情况下,也不需要担心线程问题。

我可以参考你到WWDC 2021视频Swift并发性: 背后的原理。这个视频可能不会回答你的具体问题(因为这是一种新的范式,你通常不会总是找到与我们旧的、熟悉的GCD模式直接对应的类比),但它会让你了解底层的线程模型。


你提出的问题:

  1. 关于async函数:如果我们想要在主线程和其他线程上运行相同的异步函数,你不能像使用DispatchQueue那样做...

    正确。或者重新表达一下(避免使用“线程”术语):一个函数要么隔离在特定的执行器中(例如主执行器或其他执行器),要么不是。执行器主要用于提供对某些可变状态的线程安全访问。如果它是与执行器隔离的(例如,因为它需要改变执行器的一些属性),它只能隔离到该特定的执行器。编译器会为我们处理所有线程细节,并在编译时验证我们的代码是否线程安全。

    根据这一点,将某个方法隔离到两个不同的执行器可能没有太多意义。方法的内容/功能决定了它隔离到哪个执行器。如果它不与执行器隔离的内容进行交互,我们可能会将其标记为nonisolated。因此,是的,“主执行器”恰好使用主线程来进行执行器隔离,但除此之外,我们不需要关心代码运行在哪个线程上,而是关心它是否隔离在哪个执行器上(如果隔离的话)。Swift并发性会为我们处理所有与线程相关的逻辑。

    最终效果是,使用Swift并发性,您可以享受到代码的编译时验证,确保代码始终在正确的执行器上运行。这使我们摆脱了GCD中的一个基本问题,即我们必须希望调用者将函数分派到正确的队列,并处理如果偶然未能这样做时的不可预测的运行时行为。我们必须使用TSAN、分派谓词或“主线程检查器”来希望在运行时捕获线程错误。使用Swift并发性,编译器主要确保了我们的代码的正确性。

  2. …您将不得不声明两个单独的async函数,并且其中一个标记为@MainActor,在代码级别显示代码将在哪里运行。

    从技术上讲是正确的,但实际上,如果你发现自己倾向于编写两个单独的函数,可能存在更深层次的设计问题。通常情况下,要么是在做一些需要主执行器的事情(例如,更新UI;与执行器隔离的内容进行交互等),要么不是。

    我们实际上无法在没有看到必须隔离到主执行器和其他执行器的某些实际函数示例的情况下对这个问题作出实际回答。但最好将这个问题单独提到Stack Overflow上。

  3. 关于Tasks:如果Task不在主执行器上运行,没有办法安排单独的Task以串行方式运行...

    如果您调用具有await暂停点的async方法,即使主执行器也无法保证串行执行。是的,它会确保代码不会同时并行运行,但它可以在隔离到该特定执行器的各种任务之间交错运行。

    Swift并发性是“可重入的”...一旦触发await暂停点(无论在哪个执行器上运行,无论是主执行器还是其他执行器),该特定任务都会被暂停,执行器可以自由运行等待它的其他任务。有关更多信息,请参阅SE-0306 - 执行器 - 执行器可重入性。值得注意的是,该提案考虑到将来可能引入非可重入性,但目前还没有。

    (个人认为,非可重入性,或者更好的说,一些受限并发性,将会受到极大的欢迎。在WWDC 2023的超越结构化并发的基础知识中提到的受限并发性模式,在我看来,令人难堪。他们怎么能认为这是一种可接受的模式呢?虽然它有效,但很丑陋。)

  4. …相反,您将不得不将每个任务分别设为async函数,并在一个接一个地等待它们。

    是的,您可以等待先前的Task。或者有时我们会使用AsyncSequence 类型,如AsyncStreamAsyncChannel

所以,如果你的问题基本上是,“Swift并发性似乎与GCD非常不同”,那么答案是,是的,它确实不同。

我建议你不要担心理论上的差异,而是专注于在重构代码以采用Swift并发性时遇到的实际问题。你在这里提出的问题的答案在抽象层面上无疑会感到模

英文:

tl;dr

In short, when using Swift concurrency, you should not dwell on direct analogs of GCD patterns. And you shouldn’t generally worry about threads at all.

I might refer you to WWDC 2021 video Swift concurrency: Behind the scenes. It won’t answer many of your specific questions (because this is a new paradigm; you simply will not always find direct analogs to our old, familiar GCD patterns), but it will give you insights regarding the underlying threading model.


You asked:

> 1. Regarding async functions: If we wanted to run the same async function sometimes on the main thread and at other times on other threads, you cannot do so like you would with DispatchQueues …

Correct. Or to rephrase it (avoiding “thread” terminology): A function is either isolated to a particular actor (e.g., whether the main actor or another actor) or it isn’t. Actors are primarily to provide thread-safe access to some mutable state. If it is actor-isolated (e.g., because it needs to mutate some of the actor properties), it is isolated to that particular actor only. The compiler takes care of all of the threading details for us and validates our code for thread-safety at compile-time.

In light of this, it doesn’t make too much sense to have some method isolated to two different actors. The content/functionality of the method dictates to which actor it is isolated. And if it is not interacting with actor-isolated content, we would probably make it nonisolated. So, yes, the “main actor” happens to use the main thread for this actor-isolation, but beyond that, we do not worry about on which thread particular code runs, but rather which actor it is isolated to (if isolated at all). Swift concurrency handles all of the thread-related logic for us.

The net effect is that with Swift concurrency, you enjoy compile-time validation of your code, ensuring that code always runs on the correct actor. This gets us away from the fundamental problem in GCD, where one had to hope that the caller dispatched a function to the correct queue and deal with unpredictable runtime behaviors if you happened to fail to do so. We had to use TSAN, dispatch predicates, or “main thread checker” to hopefully catch threading errors at runtime. With Swift concurrency, the compiler largely ensures the correctness of our code.

> 1. … You would have to declare two separate async functions and have one annotated @MainActor, showing at the code level where things will be run.

Technically correct, but in practice, if you find yourself inclined to write two separate functions, there probably is a deeper design problem. One is generally either doing something that requires the main actor (e.g., updating the UI; interacting with actor-isolated content; etc.) or not.

We really cannot answer this question practically without seeing a real-world example of a function that must be actor isolated, but sometimes isolated to the main actor and sometimes to some other actor. But that is probably best posed as a separate question on Stack Overflow.

> 2. Regarding Tasks: If Tasks are not run on the main actor, there is no way to schedule separate Tasks to run in a serial manner …

If you are calling async methods that have await suspension points, not even the main actor will ensure serial execution. Yes, it will make sure that code will not run in parallel at the same time, but it is free to interleave between various tasks isolated to that particular actor.

Swift concurrency is “reentrant” … as soon as you hit an await suspension point (regardless of what actor you are running on … whether the main actor or some other actor), that particular task is suspended, and the actor is free to run other tasks that are awaiting it. For more information, see SE-0306 - Actors - Actor reentrancy. FWIW, that proposal contemplates eventually introducing non-reentrancy, but not as of now.

(Personally, I think non-reentrancy, or, better, some constrained concurrency, would be greatly welcomed. The constrained concurrency pattern suggested in WWDC 2023’s Beyond the basics of structured concurrency is, IMHO, just embarrassing. How can they think that is an acceptable pattern? It works, but it is ugly.)

> 2. … instead, you would have to make each task an async function respectively and await one after another.

Yep, you can await the prior Task. Or sometimes we reach for AsyncSequence types, such as AsyncStream or AsyncChannel.


So, if your question is, fundamentally, “Swift concurrency seems very different than GCD,” then the answer is that, yes, it is.

I would suggest that you do not worry about the theoretical differences, and instead focus on actual practical problems you are seeing as you refactor your code to adopt Swift concurrency. The answers to the questions you pose here will undoubtedly feel vaguely (extremely?) dissatisfying in the abstract. But if we had concrete, real-world examples, we can probably point you in the right direction.

So, I would suggest that you don’t worry about how different Swift concurrency is from GCD, but rather focus on practical, real-world problems you encounter as you refactor your code. (And, no offense, but make sure to research that on Stack Overflow, because many of these real-world problems have been asked and answered many times already.) But if you have a MRE that is presenting a specific challenge that is not already answered on Stack Overflow, then please post a new question with that specific example.

As a final closing point, I think WWDC 2021 Swift concurrency: Update a sample app, if you have not seen it already, is a great, practical example of how we refactor legacy code to adopt Swift concurrency.

huangapple
  • 本文由 发表于 2023年7月7日 07:43:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/76633127.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定