我是否应该在“MainActor”视图模型中声明“非隔离”或“异步”函数,以便在 MainActor 上下文内部和外部运行?

Adi*_*ain 1 swift swiftui swift-concurrency

介绍

我有一个视图模型声明如下:

@MainActor class ContentViewModel: ObservableObject {
    ...
}
Run Code Online (Sandbox Code Playgroud)

我想向该模型添加一个函数,该函数在MainActor上下文内部和外部混合执行工作。

我进行了一些实验,并提出了两个实现此目的的选项,如下所述。

选项1

在视图模型中将新函数标记为nonisolated并在该函数内启动一个任务,如下所示:

nonisolated func someFunction() {
    Task {
        // Do work here.
        // Use 'await' to make calls to the 'MainActor' context
        // and to make other asynchronous calls.
    }
}
Run Code Online (Sandbox Code Playgroud)

在视图中调用该函数如下:

Button("Execute") {
    viewModel.someFunction()
}
Run Code Online (Sandbox Code Playgroud)

选项2

在视图模型中将新函数标记为async并省略任务启动,如下所示:

func someFunction() async {
    // Do work here.
    // No need to 'await' to make calls to the 'MainActor' context
    // given that this function runs in the 'MainActor' context.
    // Use 'await' to make asynchronous calls
    // and to free up the 'MainActor' whilst those calls execute.
}
Run Code Online (Sandbox Code Playgroud)

在视图中调用该函数如下:

Button("Execute") {
    Task {
        await viewModel.someFunction()
    }
}
Run Code Online (Sandbox Code Playgroud)

问题

上述两个选项中的任何一个是否比另一个选项具有优势,或者它们在功能上是否相同?是否有一个替代选项对于 Swift 和 SwiftUI 来说更惯用,并且比上述两个选项更具优势?

更新

当我最初问这个问题时,我的印象是我必须使用上面的选项 2来await调用上下文。这是不正确的。MainActorsomeFunction()

我已经编辑了选项 2 中代码块中的注释,以删除这个不正确的假设,这样我就不会误导将来偶然发现这个问题的任何人。

Rob*_*Rob 6

为了回答有关如何从当前演员身上消除这种情况的问题,有多种方法:

\n
    \n
  • 将其移动到自己的位置actor\xe2\x80\xa6 中,如果存在某些共享的可变状态或需要防止多个调用导致并行执行,则这是更好的选择,但如果目的只是从主要参与者中获取单个函数,则可能有点过分了;
  • \n
  • 让它启动一个detached任务 \xe2\x80\xa6 这是一种让当前参与者工作的简单方法,但需要非结构化并发,给作者带来手动取消处理的负担等;或者
  • \n
  • 使其成为非隔离async函数,如SE-0338 \xe2\x80\xa6 中所述,这使我们处于结构化并发领域及其带来的好处,同时使我们摆脱当前的参与者。
  • \n
\n
\n

这就引出了一个问题:你是否需要摆脱someFunction主角。事实上,该函数随后将 \xe2\x80\x9cmake 调用MainActor\xe2\x80\x9d,这表明您也应该将此函数保留在主要参与者上。

\n

someFunction因此,要确保永远不会阻塞主要参与者,需要考虑一些注意事项:

\n
    \n
  1. 如果这段代码只是await其他async方法,那么就不用担心阻塞主线程/参与者。一旦任务遇到await挂起点,当前任务就会被挂起,但当前参与者(即主要参与者)会在异步代码运行时被释放以执行其他任务。当前线程/参与者不会被阻塞。

    \n

    例如,请考虑以下情况:

    \n
    func someFunction() async {\n    let response = await someThingElse()   // this is asynchronous and will not block\n    self.objects = results.objects         // now update property isolated to the main actor with no `await`\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    如果你采用await其他async方法,现在someFunction是否与主要演员隔离已经变得无关紧要了。一旦someFunction遇到await暂停点,当前的 Actor 就会被释放来执行其他任务,同时异步工作正在进行中。正如你所看到的,如果someFunction稍后需要对主要参与者执行某些操作,这会使其意图变得清晰并简化您的代码。

    \n
  2. \n
  3. 如果someFunction在继续对主要参与者执行更多操作之前执行任何缓慢且同步的工作(例如,文件 i/o 等),则将该同步工作(并且仅该工作)移至其自己的函数中。您可以从主要参与者中获取这个新函数(例如,非隔离async函数、独立任务、其自己的参与者等等)。但是,一旦带有同步代码的这个新函数离开了主要参与者,您就可以继续someFunction在主要参与者上await调用这个新函数。这简化了someFunction,使其意图清晰(它将在某个时候使用主要演员),但让主要演员摆脱缓慢而同步的工作。

    \n

    同样,通过这种模式,主要参与者将不会被阻止,并且只有缓慢的同步代码才会从当前参与者中移出。

    \n
  4. \n
\n
\n

顺便说一句,我会避免不必要地引入非结构化并发Task {\xe2\x80\xa6}。对于非结构化并发,您承担处理任务取消的全部责任。例如,请注意,如果您像选项 1 中那样丢弃生成的任务引用,则 \xe2\x80\x9c 会使您无法显式取消该任务。\xe2\x80\x9d 正确处理取消Task {\xe2\x80\xa6}工作; 如果您保持结构化并发,您就可以免费获得这些行为。如果可以的话,避免非结构化并发。

\n

我还建议您也避免引入分离任务,除非绝对必要,因为这也会引入非结构化并发。如文档所示所说:

\n
\n

如果可以使用结构化并发功能(如子任务)对操作进行建模,则不要使用分离任务。子任务继承父任务\xe2\x80\x99的优先级和任务本地存储,取消父任务会自动取消其所有子任务。您需要使用分离任务手动处理这些注意事项。

\n
\n