如何取消由异步操作启动并返回可取消类型的`async`函数

4
我需要支持取消一个返回可以在初始化后被取消的对象的函数。在我的情况下,requester 类在一个我无法修改的第三方库中。
actor MyActor {

    ...

    func doSomething() async throws -> ResultData {

        var requestHandle: Handle?
    
        return try await withTaskCancellationHandler {
            requestHandle?.cancel() // COMPILE ERROR: "Reference to captured var 'requestHandle' in concurrently-executing code"
        } operation: {

            return try await withCheckedThrowingContinuation{ continuation in
            
                requestHandle = requester.start() { result, error in
            
                    if let error = error
                        continuation.resume(throwing: error)
                    } else {
                        let myResultData = ResultData(result)
                        continuation.resume(returning: myResultData)
                    }
                }
            }
        }
    }

    ...
}

我查看了其他SO问题和该帖子:https://forums.swift.org/t/how-to-use-withtaskcancellationhandler-properly/54341/4

有些情况非常相似,但还不完全相同。这段代码无法编译,因为出现了以下错误:

"Reference to captured var 'requestHandle' in concurrently-executing code"

我猜测编译器试图保护我免受在初始化之前使用requestHandle的影响,但我不确定如何解决这个问题。Swift论坛讨论线程中显示的其他示例似乎都有一个模式,在调用其start函数之前可以初始化requester对象。

我还尝试将requestHandle保存为类变量,但在同一位置收到了不同的编译错误:

Actor-isolated property 'profileHandle' can not be referenced from a Sendable closure

2个回答

7
您说: > 我觉得编译器试图保护我免受在 `requestHandle` 初始化之前使用它的影响。
更准确地说,它只是保护您免受竞争的影响。您需要同步与“请求者”和那个 `Handle` 的交互。 > 但我不确定如何解决这个问题。在 Swift 论坛讨论线程中显示的其他示例似乎都有一个模式,即 `requester` 对象可以在调用其 `start` 函数之前进行初始化。
是的,这正是您应该做的。不幸的是,您没有分享您的 `requester` 在哪里进行初始化或者它是如何实现的,因此我们很难对您的特定情况发表评论。
但是根本问题是您需要同步您的 `start` 和 `cancel`。因此,如果您的 `requester` 还没有这样做,您应该将其封装在一个提供线程安全交互的对象中。在 Swift 并发中,标准的方法是使用 actor。
例如,假设您正在包装一个网络请求。为了使您的访问与此同步,您可以创建一个 actor:
actor ResponseDataRequest {
    private var handle: Handle?

    func start(completion: @Sendable @escaping (Data?, Error?) -> Void) {
        // start it and save handle for cancelation, e.g.,
        
        handle = requestor.start(...)
    }

    func cancel() {
        handle?.cancel()
    }
}

这个演员开始和取消了一个网络请求。然后你可以做一些像这样的事情:

func doSomething() async throws -> ResultData {
    let responseDataRequest = ResponseDataRequest()

    return try await withTaskCancellationHandler {
        Task { await responseDataRequest.cancel() }
    } operation: {
        return try await withCheckedThrowingContinuation { continuation in
            Task {
                await responseDataRequest.start { result, error in
                    if let error = error {
                        continuation.resume(throwing: error)
                    } else {
                        let resultData = ResultData(result)
                        continuation.resume(returning: resultData)
                    }
                }
            }
        }
    }
}

当你验证了所有已检查的继续运行没有问题时,显然可以转向不安全的继续运行。

我在问题中没有提到doSomething函数已经在一个actor中,我尝试做类似于您建议的事情(我更新了问题)。但是看起来用Task{ await requestHandle?.cancel() }包装取消处理块是关键。顺便说一下,如果我在withCheckedThrowingContinuation闭包的主体中删除Task包装,它也可以编译通过 - 所以不确定这是否对您的修复至关重要?还没有测试它是否支持取消或经受得住压力测试。甚至不确定如何为此编写单元测试。 - Daniel
1
正确。withCheckedThrowingContinuation体中的Task包装器仅在介绍单独的本地Actor变量以同步取消处理程序时才需要。 - Rob

-1

再次查看Swift讨论线程后,我发现你可以这样做:

...
var requestHandle: Handle?

let onCancel = { profileHandle?.cancel() }

return try await withTaskCancellationHandler {
    onCancel()
}
...

4
我会尽力为您翻译这段内容。"我会对使用onCancel闭包模式持谨慎态度。它实际上隐藏了底层问题,在Swift 6中这将开始产生错误。例如,尝试添加“其他Swift标志”构建设置 -Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks。" - Rob

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