如何避免我的 Swift 异步方法在 SwiftUI 的主线程上运行?

Mar*_*ark 13 concurrency task swift swiftui

我观看了所有关于 async/await (和演员)的视频,但我仍然有点困惑。

因此,假设我有一个异步方法:func postMessage(_ message: String) async throws并且我有一个简单的 SwiftUI 视图。

@MainActor 
struct ContentView: View {
    @StateObject private var api = API()
    
    var body: some View {
        Button("Post Message") {
            Task {
                do {
                    try await api.postMessage("First post!")
                } catch {
                    print(error)
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我明确告诉 SwiftUI 使用 ,@MainActor尽管我知道它会从 推断出来@StateObject

据我了解,由于我们使用该@MainActor工作是在主线程上完成的。这意味着任务上的工作也将在主线程上完成。这不是我想要的,因为上传过程可能需要一些时间。在这种情况下,我可以Task.detached使用不同的线程。这就能解决它。如果我的理解是正确的话。

现在让它变得更复杂一点。如果... postMessage 会返回一个整数形式的帖子标识符,并且我想在视图中呈现它,该怎么办?

struct ContentView: View {
    @StateObject private var api = API()
    @State private var identifier: Int = 0
    
    var body: some View {
        Text("Post Identifier: \(String(describing: identifier))")
        Button("Post Message") {
            Task {
                do {
                    identifier = try await api.postMessage("First post!")
                } catch {
                    identifier = 0
                    print(error)
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这将起作用(再次根据我的理解)任务在主线程上运行。如果我现在将其更改为,Task.detached我们将收到错误"Property 'identifier' isolated to global actor 'MainActor' can not be mutated from a non-isolated context"

这是有道理的,但是我们如何将值返回给主要参与者以便更新视图呢?

也许我的假设是错误的。让我们看看我的 API 类。

actor API {
    func postMessage(_ message: String) async throws -> Int {
        // Some complex internet code
        return 0
    }
}
Run Code Online (Sandbox Code Playgroud)

由于 API 在其自己的 actor 中运行。互联网工作也会在不同的线程上运行吗?

Mic*_*nat 20

问题很好,答案很复杂。我在这个主题上花费了大量时间,有关详细信息,请点击评论中的 Apple 开发者论坛链接。

  • 下面提到的所有任务都是非结构化任务,例如。由...制作Task ...

  • 这是关键:“主要参与者是代表主线程的参与者。主要参与者通过主调度队列执行所有同步。

  • 由例如创建的非结构化任务Task.initTask { ... }继承参与者异步上下文。

  • 分离的任务Task.detachedasync let =任务、组任务不继承参与者异步上下文。

  • 示例 1:let task1 = Task { () -> Void in ... }创建并启动新任务,该任务从调用点继承优先级和异步上下文。当在主线程上创建时,任务将在主线程上运行。

  • 示例 2:let task1 = Task.detached { () -> Void in ... }创建并启动不继承优先级或异步上下文的新任务。该任务将在某个线程上运行,很可能在当前线程以外的其他线程上运行。执行者决定。

  • 示例 3:let task1 = Task.detached { @MainActor () -> Void in ... }创建并启动新任务,该任务不继承优先级也不继承异步上下文,但该任务将在主线程上运行,因为它是这样注释的。

  • 很可能,任务将包含至少一个awaitasync let =命令。这些命令是结构化并发的一部分,您无法影响隐式创建的任务(此处根本不讨论)在哪个线程上执行。Swift 执行者决定这一点。

  • 继承的 Actor 异步上下文与线程无关,每个await线程可能会更改之后,但是 Actor 异步上下文在所有非结构化任务中保持不变(是的,可能位于各个线程上,但这对于执行器来说很重要)。

  • 如果继承的 Actor 异步上下文是 MainActor,则任务从开始到结束都在主线程上运行,因为 Actor 上下文是 MainActor。如果您计划运行一些真正并行的计算,这一点很重要 - 确保所有非结构化任务不在同一线程上运行。

  • 在这两种情况下,ContentView 都在 @MainActor 上:第一种情况是明确的 @MainActor,第二种情况使用 @StateObject 属性包装器,即 @MainActor,因此整个 ContentView 结构是 @MainActor 推断的。https://www.hackingwithswift.com/quick-start/concurrency/understanding-how-global-actor-inference-works

  • async let = 是一种结构化并发,它不继承 Actor 异步上下文,并按照执行程序的计划立即并行运行(在其他线程上,如果可用)

  • 上面的示例有一个系统缺陷:@StateObject private var api = API()@MainActor。这是由 @StateObject 强制的。因此,我建议将其他 actor 与其他 actor 异步上下文作为依赖项注入,而不使用 @StateObject。async/await 将真正发挥作用,使await 调用保持在正确的actor 上下文中。


Rob*_*Rob 7

你说:

\n
\n

据我了解,由于我们使用该@MainActor工作是在主线程上完成的。意味着工作Task也将在主线程上完成。这不是我想要的,因为上传过程可能需要一些时间。

\n
\n

只是因为上传过程 (a) 可能需要一些时间;(b) 是从主要演员处调用的,这并不在执行请求时主线程将被阻塞。

\n

事实上,这就是 Swift concurrency\xe2\x80\x99s 的全部意义awaitawait当我们从被调用的例程中得到结果时,它会释放当前线程去做其他事情。不要将awaitSwift 并发性(不会阻塞调用线程)与各种并发性混为一谈。wait传统 GCD 模式的各种构造(会阻塞)混为一谈。

\n

例如,考虑从主要参与者启动的以下代码:

\n
Task {\n    do {\n        identifier = try await api.postMessage("First post!")\n    } catch {\n        identifier = 0\n        print(error)\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Task是的,因为您从主要参与者使用并调用了它,所以该代码也将在主要参与者上运行。因此,identifier将更新主要演员。但它没有说明正在postMessage使用哪个参与者/队列/线程。该关键字的意思是 \xe2\x80\x9cI\' 将暂停这条执行路径,并让主线程在我们发起其网络请求和最终响应await时去做其他事情。\xe2\x80\x9dawaitpostMessage

\n

你问:

\n
\n

让我们看看我的 API 类。

\n
actor API {\n    func postMessage(_ message: String) async throws -> Int {\n        // Some complex internet code\n\n        return 0\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

由于 API 在其自己的 actor 中运行。互联网是否也可以在不同的线程上运行?

\n
\n

作为一般规则,网络库异步执行其请求,不会阻塞调用它们的线程。假设 \xe2\x80\x9ccomplex 互联网代码\xe2\x80\x9d 遵循此标准约定,那么您就不必担心它在哪个线程上运行。请求运行时,actor\xe2\x80\x99s 线程不会被阻塞。

\n

更进一步,事实是它API自己的演员对于这个问题来说并不重要。即使API在主要参与者上,由于网络请求是异步运行的,因此无论使用什么线程都API不会被阻塞。

\n

(注意:上面假设 \xe2\x80\x9c 复杂的互联网代码 \xe2\x80\x9d 遵循传统的异步网络编程模式。如果没有看到此代码的代表性示例,我们显然无法进一步评论。)

\n
\n

如果您对 Swift 并发如何为我们管理线程感兴趣,请观看 WWDC 2021 视频Swift 并发:幕后花絮。

\n