如何防止 Actor 重入导致重复请求?

Rob*_*Rob 34 async-await swift swift-concurrency

在 WWDC 2021 视频中,使用 Swift Actor 保护可变状态中,他们提供了以下代码片段:

\n
actor ImageDownloader {\n    private var cache: [URL: Image] = [:]\n\n    func image(from url: URL) async throws -> Image? {\n        if let cached = cache[url] {\n            return cached\n        }\n\n        let image = try await downloadImage(from: url)\n\n        cache[url] = cache[url, default: image]\n\n        return cache[url]\n    }\n\n    func downloadImage(from url: URL) async throws -> Image { ... }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

问题是 actor 提供可重入性,因此cache[url, default: image]引用有效地确保即使您由于某些竞争而执行了重复的请求,您至少在继续之后检查 actor\xe2\x80\x99s 缓存,确保您获得相同的图像对于重复的请求。

\n

在那段视频中,他们

\n
\n

更好的解决方案是完全避免冗余下载。我们\xe2\x80\x99 已将该解决方案放入与该视频相关的代码中。

\n
\n

但网站上没有与该视频相关的代码。那么,更好的解决方案是什么?

\n

我了解 Actor 可重入的好处(如SE-0306中所述)。例如,如果下载四个图像,则不希望禁止重入,从而失去下载的并发性。实际上,我们希望等待对特定图像的重复先前请求的结果(如果有),如果没有,则启动一个新的downloadImage.

\n

rob*_*off 30

更新

\n

Apple\xe2\x80\x99s 开发者网站现在包含 WWDC 视频的代码片段(至少在 2021 年及以后)。您可以在视频\xe2\x80\x99s页面上找到\xe2\x80\x9c更好的解决方案\ xe2\x80\x9d代码,方法是点击视频播放器下的\xe2\x80\x9cCode\xe2\x80\x9d选项卡并滚动下降到 \xe2\x80\x9c11:59 - 在等待后检查您的假设:更好的解决方案\xe2\x80\x9d。

\n

原来的

\n

您可以在开发者应用程序中找到\xe2\x80\x9cbetter解决方案\xe2\x80\x9d\xc2\xa0code 。在开发人员应用程序中打开会话,选择“代码”选项卡,然后滚动到 \xe2\x80\x9c11:59 - 等待后检查您的假设:更好的解决方案\xe2\x80\x9d。

\n

开发者应用程序的屏幕截图

\n

屏幕截图来自我的 iPad,但开发者应用程序也可以在 iPhone、Mac 和 Apple TV 上使用。(我不知道Apple TV版本是否提供了查看和复制代码的方法,不过\xe2\x80\xa6)

\n

据我所知,该代码在developer.apple.com 网站上不可用,无论是在WWDC 会议的页面上还是作为示例项目的一部分。

\n

为了方便后代,这里是苹果的代码。它与安迪·伊巴内斯 (Andy Ibanez) 极其相似:

\n
actor ImageDownloader {\n\n    private enum CacheEntry {\n        case inProgress(Task<Image, Error>)\n        case ready(Image)\n    }\n\n    private var cache: [URL: CacheEntry] = [:]\n\n    func image(from url: URL) async throws -> Image? {\n        if let cached = cache[url] {\n            switch cached {\n            case .ready(let image):\n                return image\n            case .inProgress(let task):\n                return try await task.value\n            }\n        }\n\n        let task = Task {\n            try await downloadImage(from: url)\n        }\n\n        cache[url] = .inProgress(task)\n\n        do {\n            let image = try await task.value\n            cache[url] = .ready(image)\n            return image\n        } catch {\n            cache[url] = nil\n            throw error\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n


Rob*_*Rob 10

在我想出最初的答案后,我偶然发现了 Andy Ibanez\xe2\x80\x99s 的文章《Understanding Actors in the New Concurrency Model》,其中他没有提供 Apple\xe2\x80\x99s 代码,但提供了一些东西受到它的启发。这个想法非常相似,但他使用枚举来跟踪缓存和待处理的响应:

\n
actor ImageDownloader {\n    private enum ImageStatus {\n        case downloading(_ task: Task<UIImage, Error>)\n        case downloaded(_ image: UIImage)\n    }\n    \n    private var cache: [URL: ImageStatus] = [:]\n    \n    func image(from url: URL) async throws -> UIImage {\n        if let imageStatus = cache[url] {\n            switch imageStatus {\n            case .downloading(let task):\n                return try await task.value\n            case .downloaded(let image):\n                return image\n            }\n        }\n        \n        let task = Task {\n            try await downloadImage(url: url)\n        }\n        \n        cache[url] = .downloading(task)\n        \n        do {\n            let image = try await task.value\n            cache[url] = .downloaded(image)\n            return image\n        } catch {\n            // If an error occurs, we will evict the URL from the cache\n            // and rethrow the original error.\n            cache.removeValue(forKey: url)\n            throw error\n        }\n    }\n    \n    private func downloadImage(url: URL) async throws -> UIImage {\n        let imageRequest = URLRequest(url: url)\n        let (data, imageResponse) = try await URLSession.shared.data(for: imageRequest)\n        guard let image = UIImage(data: data), (imageResponse as? HTTPURLResponse)?.statusCode == 200 else {\n            throw ImageDownloadError.badImage\n        }\n        return image\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n


Rob*_*Rob 5

关键是保留对 的引用Task,如果找到,则保留awaitvalue.

也许:

actor ImageDownloader {
    private var cache: [URL: Image] = [:]
    private var tasks: [URL: Task<Image, Error>] = [:]

    func image(from url: URL) async throws -> Image {
        if let image = try await tasks[url]?.value {
            print("found request")
            return image
        }

        if let cached = cache[url] {
            print("found cached")
            return cached
        }

        let task = Task {
            try await download(from: url)
        }

        tasks[url] = task
        defer { tasks[url] = nil }

        let image = try await task.value
        cache[url] = image

        return image
    }

    private func download(from url: URL) async throws -> Image {
        let (data, response) = try await URLSession.shared.data(from: url)
        guard
            let response = response as? HTTPURLResponse,
            200 ..< 300 ~= response.statusCode,
            let image = Image(data: data)
        else {
            throw URLError(.badServerResponse)
        }
        return image
    }
}
Run Code Online (Sandbox Code Playgroud)