如何正确取消 Swift 的 async/await 函数

16

我观看了在Swift中探索结构化并发视频以及其他相关的视频/文章/书籍(来自Sundell的Swift,Hacking with Swift,Ray Renderlich),但那里的所有示例都非常琐碎 - 异步函数通常只有一个异步调用。在实际代码中应该如何处理?

例如:

...
        task = Task {
            var longRunningWorker: LongRunningWorker? = nil

            do {
                var fileURL = state.fileURL
                if state.needsCompression {
                    longRunningWorker = LongRunningWorker(inputURL: fileURL)
                    fileURL = try await longRunningWorker!.doAsyncWork()
                }

                let urls = try await ApiService.i.fetchUploadUrls()

                if let image = state.image, let imageData = image.jpegData(compressionQuality: 0.8) {
                    guard let imageUrl = urls.signedImageUrl else {
                        fatalError("Cover art supplied but art upload URL is nil")
                    }

                    try await ApiService.i.uploadData(url: imageUrl, data: imageData)
                }

                let fileData = try Data(contentsOf: state.fileUrl)
                try await ApiService.i.uploadData(url: urls.signedFileUrl, data: fileData)

                try await ApiService.i.doAnotherAsyncNetworkCall()
            } catch {
                longRunningWorker?.deleteFilesIfNecessary()
                throw error
            }


        }
...

在某个时候,我将调用 task.cancel()

是谁负责取消什么?到目前为止,我看到的示例都会使用 try Task.checkCancellation(),但对于这段代码来说,那行代码应该每隔几行出现一次 - 是这样做吗?

如果API服务使用URLSession,则在iOS 15上调用将被取消,但我们不使用URLSession代码的异步变体,因此必须手动取消调用。此外,所有长时间运行的工作程序代码也适用于此。

我还在考虑是否可以在每个异步函数中添加此检查,但是这样基本上所有异步函数都将具有相同的模板代码,这似乎是错误的,并且我没有在任何视频中看到过这种做法。

编辑:已删除回调调用,因为它们与问题无关。

3个回答

11

我们实现自己的取消逻辑有两种基本模式:

  1. 使用withTaskCancellationHandler(operation:onCancel:)来包装可取消的异步进程。

    当调用可取消的遗留API并将其包装在Task中时,这很有用。这样,取消任务可以主动停止您的遗留API中的异步进程,而不是等到手动调用isCancelledcheckCancellation。此模式适用于iOS 13/14 URLSession API或任何提供取消方法的异步API。

  2. 定期检查isCancelled或使用try checkCancellation

    这在您正在执行具有循环的某些手动计算密集型过程的情况下非常有用。

    许多关于处理协作式取消的讨论往往会涉及这些方法,但在处理遗留可取消API时,前面提到的withTaskCancellationHandler通常是更好的解决方案。

因此,我个人会重点关注在包装某些遗留异步进程的方法中实现协作式取消。通常,取消逻辑将向上移动,不需要在调用链中进一步进行检查,往往由您可能已经拥有的任何错误处理逻辑处理。


谢谢你的回答。withTaskCancellationHandler中的handler部分仅用于清理吗?还是该部分也可能引发取消错误?我想我现在看到的主要问题是我的父VC正在等待子VC的任务完成以关闭子VC。此外,当用户点击取消时,必须关闭此模态。如果我使用checkCancellation(),则模态只会在当前网络调用结束后关闭。取消处理程序会立即触发,但不知道是否有一种方法可以通过抛出错误立即停止父任务执行。 - Raimundas Sakalauskas
我没有例子无法确定。我建议您创建一个最小化、完整且可重现的示例来描述问题,并将其作为新问题发布。 - Rob

2

我想借助10个额外月份的async / await经验来回答我的问题。

如果我今天要编写这样的函数,我不会在此任务块内检查取消。我会调用这个“管理器”任务,它所依赖的所有函数都称为“工作人员”任务,因为它们执行实际的长时间运行工作。

所有的“工作人员”函数在执行工作之前都应该检查取消。如果其中任何一个函数注意到任务被取消,那么它应该抛出取消错误,从本质上终止父任务(除非调用者使用try/catch块并要求在抛出错误时进行回退)。

我只会在“管理器”任务中明确检查取消,仅当在取消后完成“工作人员”任务可能会破坏状态时。


这正是我的答案所说的。“请注意,如果在您的任务中,您使用try调用任何在取消时抛出异常的异步材料,则无需对该材料进行任何取消工作,因为当它由于取消而抛出异常时,您将自动由于取消而抛出异常。”有耳可听者听之。 - matt
正确,但考虑到问题的赞数比答案高,我认为人们并不认为那个答案很有帮助,这是我的看法。 - Raimundas Sakalauskas
完全正确。我只是想指出,已经尝试引导您朝着正确的方向前进。您完全可以添加自己的答案!这是鼓励的。但我有点犹豫,因为“Stack Overflow不是您的博客”的原则。既然问题已经得到了正确的回答,您通过自传式的轶事重复正确的答案并没有什么收获。您所学到的教训很有价值,但我建议这不是它的适当位置。问答周期已经结束。 - matt
此外,如果“假设问题的赞数比答案多”被认为是一个论点,那么这是错误的。这些赞数意味着“表达得很好”或者“是的,我也有同样的问题,谢谢你替我问出来”。它并不意味着“我仍然对此感到困惑”,因为页面上的答案_也回答了这个问题_。计算问题的投票数与答案的投票数并不能告诉你任何东西。 - matt
1
最后的想法:简洁地表达,这可能比单独回答更合适。好了,我不再给你添麻烦了!:) 对此我很抱歉。 - matt

1
到目前为止我看到的例子都使用 try Task.checkCancellation(),但对于这段代码来说,该行应该出现在每几行中 - 是这样做的吗?
基本上是的。取消是完全自愿的。运行时不知道取消对于你特定的任务意味着什么,所以它就由你决定。你可以查看Task.isCancelled,或者如果你的意图是为了抛出异常以防任务被取消,你可以调用Task.checkCancellation。
请注意,如果在任务内部调用任何异步材料(使用try),当取消时会引发异常,因此您不需要针对该材料进行任何取消工作,因为当它由于取消而引发异常时,您将自动由于取消而抛出异常。
尽管如此,我还要补充一点,作为脚注,你的代码非常奇怪。回调和async/await是相反的;你会在任务中执行 do/catch 并调用一个回调是非常奇怪的,我建议不要这样做。这样做基本上否定了任务的所有优势,并使我刚才说的throw技巧失效。

感谢您的回复。在那段特定的代码中,回调函数的存在是为了避免一次性重构整个代码库。我确实同意混合使用回调和async/await并不是一种标准的做法。那么在这段特定的代码中,您是否会在每个await语句后添加“Task.isCancelled”检查呢?那似乎是一个非常丑陋的解决方案。 - Raimundas Sakalauskas
1
“在那段特定的代码中,回调函数的存在是为了避免一次性重构整个代码库。” 我理解,但这不是将代码重新调整为同时兼容新的async/await和旧的回调架构的方法。有一种正确的方法可以做到这一点,涉及将其包装在一个续函数中,而你没有这样做。” - matt
1
那么在这段特定的代码中,您会在每个等待语句后面简单地添加“Task.isCancelled”检查吗?那正好与我的建议相反。我建议您将每个await语句都变成try await,也就是说,每个异步方法都应该抛出异常,并且每次调用异步方法时都要进行checkCancellation调用。 - matt
有一种正确的方法可以做到这一点,涉及将其包装在一个连续函数中。我知道这一点。但是我还没有找到一种正确的方法来与UI代码结合使用。此任务在子视图控制器中执行,父视图控制器需要知道结果。现在正在查看异步属性,也许这可能是如何在此代码中消除回调的答案。 - Raimundas Sakalauskas
让您调用的每个异步方法都进行checkCancellation检查。好的,这是我考虑过的其中一种方法,可能比只在一个函数中进行所有检查更优雅(我想这更符合async/await所推广的协作式方法)。 - Raimundas Sakalauskas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接