Swift - 异步任务

521 阅读3分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

有时,在项目中可能希望自动重试失败的异步操作,例如为了解决临时网络问题,或重新建立某种形式的连接。

正在使用 Apple 的 Combine 框架来实现网络调用时这样做,在处理遇到的任何错误之前,我们将重试多达 3 次:

struct SettingsLoader {
    var url: URL
    var urlSession = URLSession.shared
    var decoder = JSONDecoder()

    func load() -> AnyPublisher<Settings, Error> {
        urlSession
            .dataTaskPublisher(for: url)
            .map(.data)
            .decode(type: Settings.self, decoder: decoder)
            .retry(3)
            .eraseToAnyPublisher()
    }
}

但是如果我们想实现类似的东西,但是使用 Swift Concurrency 来代替呢? 虽然 Combine 的 Publisher 协议将上述重试运算符作为内置 API 包含在内,但 Swift 的新并发 API 都没有提供类似的东西(至少在撰写本文时没有),所以我们必须发挥创造力!

Swift 的新并发系统,尤其是 async/await 的一个非常巧妙的方面是,它使我们能够将各种异步调用与标准控制流结构混合在一起,例如 if 语句和 for 循环。 因此,为等待标记的调用实现自动重试的一种方法是将我们想要运行的异步代码放在一个循环中,该循环在一个范围内迭代,这反过来又描述了我们希望执行多少次重试——就像这样:

struct SettingsLoader {
    var url: URL
    var urlSession = URLSession.shared
    var decoder = JSONDecoder()

    func load() async throws -> Settings {
        // Perform 3 attempts, and retry on any failure:
        for _ in 0..<3 {
            do {
                return try await performLoading()
            } catch {
                // This 'continue' statement isn't technically
                // required, but makes our intent more clear:
                continue
            }
        }

        // The final attempt (which throws its error if it fails):
        return try await performLoading()
    }

    private func performLoading() async throws -> Settings {
        let (data, _) = try await urlSession.data(from: url)
        return try decoder.decode(Settings.self, from: data)
    }
}

上面的实现工作得非常好,但是如果我们希望在整个项目的多个地方添加相同类型的重试逻辑,那么可能值得将该代码移动到某种可以轻松重用的实用程序形式中。

做到这一点的一种方法是使用方便的 API 扩展 Swift 的 Task 类型,让我们可以快速创建这样的自动重试任务。 我们的实际逻辑可以与之前几乎保持一致,但我们将参数化最大重试次数,我们还将添加对取消的支持:

image.png

这已经是一个非常有用且完全可重用的实现,但让我们更进一步,好吗?

在重试异步操作时,希望在每次重试之间增加一点延迟是很常见的——也许是为了让外部系统(例如服务器)有机会在我们再次尝试调用之前从某种错误中恢复 它。 因此,让我们也添加对此类延迟的支持,这可以使用内置的 Task.sleep API 轻松完成:

image.png

如果我们愿意,我们还可以将“半公开”@_implicitSelfCapture 属性添加到我们的操作闭包中,这将使其具有与将闭包直接传递给 Task 类型本身时相同的隐式自捕获行为。

然而,这并不是我真正推荐做的事情(考虑到下划线的属性可以随时改变),所以让我们通过重构之前的 SettingsLoader 示例来结束事情,而不是使用我们的新任务扩展来执行它的重试:

image.png