swift 我应该在我的'MainActor'视图模型中声明一个'nonisolated'或'async'函数,用于在MainActor上下文内外运行的工作吗?

mgdq6dx1  于 2023-09-30  发布在  Swift
关注(0)|答案(3)|浏览(97)

介绍

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

@MainActor class ContentViewModel: ObservableObject {
    ...
}

我想给这个模型添加一个函数,它在MainActor上下文中内外混合工作。
我已经做了一些实验,并提出了两种实现这一点的选择,如下所述。

选项一

将视图模型中的新函数标记为nonisolated,并在函数中初始化Task,如下所示:

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

在视图中调用此函数,如下所示:

Button("Execute") {
    viewModel.someFunction()
}

选项二

将视图模型中的新函数标记为async并省略Task初始化,如下所示:

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.
}

在视图中调用此函数,如下所示:

Button("Execute") {
    Task {
        await viewModel.someFunction()
    }
}

问题

上述两个选项中的任何一个是否具有优势,或者它们在功能上是相同的?有没有一个更符合Swift和SwiftUI习惯的替代选项,并且比上面的两个选项更有优势?

更新

当我最初问这个问题时,我的印象是我必须使用await从上面的选项2中的someFunction()调用MainActor上下文。这是不正确的。
我已经编辑了选项2中代码块中的注解,以删除这个错误的假设,这样我就不会误导将来遇到这个问题的任何人。

3ks5zfa0

3ks5zfa01#

在回答关于如何从当前参与者中删除此内容的问题时,有多种方法:

  • 将其移动到自己的actor中......如果存在一些共享的可变状态或需要防止由多个调用导致的并行执行,则这是优选的,但如果意图只是从主参与者获得单个函数,则可能是过度的;
  • 让它启动一个detached任务......这是一个简单的方法,可以让当前的参与者完成工作,但是需要非结构化的并发性,给作者带来了手动取消处理的负担,等等;或
  • 使其成为一个非隔离的async函数,如SE-0338中所讨论的......这使我们保持在structured concurrency的范围内,并带来好处,同时让我们摆脱当前的参与者。

这就引出了一个问题,即您是否需要将someFunction从主要参与者中删除。这个函数随后将“调用MainActor”的事实表明,您也应该将这个函数保留在主参与者上。
因此,关于确保someFunction永远不会阻止主要参与者,有一些注意事项:
1.如果这段代码将简单地await其他async方法,那么就不用担心阻塞主线程/actor。一旦任务遇到await挂起点,当前任务将被挂起,但当前参与者(即主参与者)将被释放,以在异步代码运行时执行其他任务。当前线程/参与者将不会被阻止。
例如,考虑以下内容:

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

如果您await其他async方法,那么someFunction是否与主要参与者隔离就变得无关紧要了。一旦someFunction遇到await挂起点,当前参与者就被释放,在异步工作正在进行的同时执行其他任务。如您所见,如果someFunction稍后需要对主参与者执行某些操作,这将使其意图清晰并简化代码。
1.如果someFunction在继续对主参与者执行更多操作之前执行任何缓慢的同步工作(例如,文件i/o等),则将该同步工作(并且仅该工作)移动到其自己的函数中。您可以从主参与者中获取这个新函数(例如,一个非隔离的async函数,一个分离的任务,它自己的参与者,等等)。但是,一旦这个带有同步代码的新函数离开了主参与者,您就可以将someFunction保留在主参与者上,await调用这个新函数。这简化了someFunction,明确了它的意图(它将在某个时候使用主参与者),但将缓慢和同步的工作从主参与者身上移除。
使用这种模式,主参与者也不会被阻塞,只有慢速的同步代码会从当前参与者中移出。
顺便说一句,我会避免使用Task {…}引入不必要的非结构化并发。使用非结构化并发,您将承担处理任务取消的所有责任。例如,请注意,如果您放弃了结果任务引用,就像您在选项1中所做的那样,它“使您无法显式地取消任务”。如果您保持在结构化并发中,则可以免费获得这些行为。尽可能避免非结构化并发。
我还建议您也避免引入分离的任务,除非绝对必要,因为这也会引入非结构化并发。正如documentation所说:
如果有可能使用结构化并发特性(如子任务)对操作建模,请不要使用分离任务。子任务继承父任务的优先级和任务本地存储,取消父任务将自动取消其所有子任务。您需要使用分离任务手动处理这些注意事项。

piv4azn7

piv4azn72#

我想给这个模型添加一个函数,它在MainActor上下文内外混合工作。
如果目标/问题是保证某些代码不在主要参与者上运行,那么所有代码都是偏离目标的。相反,你需要做一个不同的演员。这就是演员们所追求的。

2q5ifsrm

2q5ifsrm3#

在思考了Rob的答案(here)并继续实验Swift Concurrency之后,我现在的立场是我们应该在视图中(在视图模型之外)启动Task。1接下来的问题是我们是否应该在MainActor视图模型中定义函数:

  1. async关键字
  2. nonisolated关键字
  3. nonisolatedasync关键字;或
    1.没有nonisolatedasync关键字。
    让我们一个接一个地检查每个选项,并讨论它何时适用。

选项1 -仅使用async关键字声明函数

在视图模型中只使用async关键字声明函数(即没有nonisolated关键字),如果它进行任何需要await的调用,并且它没有进行长期阻塞调用2。

选项2 -仅使用nonisolated关键字声明函数

在视图模型中,只使用nonisolated关键字对函数进行解密(即没有async关键字),如果它进行任何长期阻塞调用,并且它不进行需要await的调用。

选项3 -使用nonisolated和async关键字声明函数

如果该函数进行的调用需要await,并且它进行的是长期阻塞调用,则在视图模型中使用asyncnonisolated关键字声明该函数。

选项4 -声明不带nonisolated和async关键字的函数

如果不进行需要await的调用,并且不进行长期阻塞调用,则在视图模型中声明函数时不使用asyncnonisolated关键字。3

摘要

MainActor视图模型中,是否将一个函数声明为nonisolatedasync或两者都不声明取决于你在函数中具体做了什么。有关上面列出的每个选项的具体示例,请参见this sample application
1这是为了让你的代码尽可能早地进入结构化并发。
2我所说的“长期阻塞调用”是指一个函数阻塞了它被调用的线程超过几毫秒。
3如果你的函数满足这两个条件,你实际上不需要在视图中启动一个任务来调用它。

相关问题