许多任务中的一种方法 async/await

Pit*_*Pan 3 task grand-central-dispatch async-await swift urlsession

您好,我有一个情况,我需要在多个任务中调用相同的方法。我希望能够一一调用此方法(同步)而不是在并行模式下。看起来像这样:

var isReadyToRefresh: Bool = true

func refresh(value: Int) async {
    try! await Task.sleep(nanoseconds: 100_000_000) // imitation API CALL
    isReadyToRefresh = false
    print("Try to refresh: \(value)")
}

func mockCallAPI(value: Int) async {
    if isReadyToRefresh {
        await refresh(value: value)
    }
}

Task {
     await mockCallAPI(value: 1)
}

Task {
     await mockCallAPI(value: 2)
}
Run Code Online (Sandbox Code Playgroud)

输出:

尝试刷新:1

尝试刷新:2

我所需的输出:

尝试刷新:1 或尝试刷新 2。取决于第一个任务被调用。

有任何想法吗?

Rob*_*Rob 8

你说:

\n
\n

我希望[第二次尝试]等待第一次刷新 API 完成

\n
\n

您可以保存对您的引用Task(如果找到)await。如果没有找到,则开始任务。(并且因为我们使用的是非结构化并发,所以请记住将其包装在 . 中withTaskCancellationHandler。)

\n

另外,我个人会将等待/取消内容的逻辑移至 \xe2\x80\x9crefresh\xe2\x80\x9d 进程中,而不是 API 调用代码中。因此:

\n
actor Refresh {\n    var priorTask: Task<Void, Error>?\n\n    func refresh(value: Int) async throws {\n        if let priorTask {\n            _ = try await priorTask.value\n            return\n        }\n\n        let task = Task {\n            try await mockCallAPI(value: value)\n        }\n\n        priorTask = task\n\n        try await withTaskCancellationHandler {\n            _ = try await task.value\n            priorTask = nil\n        } onCancel: {\n            task.cancel()\n        }\n    }\n\n    private func mockCallAPI(value: Int) async throws {\n        try await Task.sleep(for: .seconds(0.1))        // imitation API CALL\n        print("Try to refresh: \\(value)")\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

Apple 在与 WWDC 2021 视频“使用 Swift Actor 保护可变状态”相关的代码中展示了这种模式的示例。

\n

他们的示例更复杂(一种避免某些图像缓存/下载程序发起重复网络请求的模式),但其思想的核心是相同的:保存和await.Task

\n
\n

请注意,上面的内容是围绕原始问题设计的,以返回第一个请求\xe2\x80\x99s 结果,并避免在前一个请求正在进行时启动后续请求。这种模式在缓存结果模式中很常见,在这种模式中,您可能有重复的请求,所有请求都返回完全相同的结果(例如,来自 CDN 的某些静态资源,例如在 Apple 示例中)。

\n

但是当我们谈论 \xe2\x80\x9crefresh\xe2\x80\x9d 进程时,用户通常想要最新的结果。对于 \xe2\x80\x9crefresh\xe2\x80\x9d,我们通常不希望向用户显示先前请求的旧的、可能已经过时的结果。因此,在刷新时,我们通常希望取消之前的请求并开始一个新的请求:

\n
actor Refresh {\n    var priorTask: Task<Void, Error>?\n\n    func refresh(value: Int) async throws {\n        let task = Task { [priorTask] in\n            priorTask?.cancel()\n            try await mockCallAPI(value: value)\n        }\n\n        priorTask = task\n\n        try await withTaskCancellationHandler {\n            _ = try await task.value\n        } onCancel: {\n            task.cancel()\n        }\n    }\n\n    private func mockCallAPI(value: Int) async throws {\xe2\x80\xa6}\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是一个微妙的点,但请注意,我们希望捕获priorTask以避免对此刷新过程的多个调用之间的竞争。

\n

因此,您有以下两个选择: (a) 等待/返回第一个请求并避免重复请求;(b) 取消先前的请求并发起新的请求,确保刷新返回最新的结果。一般来说,当结果是静态的时,我们会倾向于使用第一种模式,而当结果可能随时间变化时,我们会倾向于使用后者。使用术语 \xe2\x80\x9crefresh\xe2\x80\x9d 通常意味着我们想要最新的结果,但这完全取决于您。我只是想展示这两种模式。

\n