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

33

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

actor ImageDownloader {
    private var cache: [URL: Image] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            return cached
        }

        let image = try await downloadImage(from: url)

        cache[url] = cache[url, default: image]

        return cache[url]
    }

    func downloadImage(from url: URL) async throws -> Image { ... }
}

问题在于演员提供了可重入性,因此cache [url,默认值:image]引用有效地确保即使由于某些竞争导致执行冗余请求,您至少会在继续之后检查演员的缓存,确保您获取冗余请求的相同图像。

而在那个视频中,他们

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

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

我理解演员的可重入性的好处(如在SE-0306中讨论的那样)。例如,如果下载四个图像,则不想禁止可重入性,从而失去下载的并发性。我们实际上希望等待特定图像的重复先前请求的结果(如果有),如果没有,则开始新的downloadImage


5
有没有地方可以投票选出“StackOverflow上最伟大的双自问自答问题”?我觉得我应该标记这个问题,让钻石版主可以将其作为其他人的示例发布。 - Rob Napier
请在开发者应用程序的“代码”选项卡中查找它。 - malhal
是的,就像下面Rob Mayoff在他的答案中指出的那样,我已经接受了。它只是网站上没有提供。 - Rob
是的,就像Rob Mayoff在他下面的回答中指出的那样,我已经接受了。当我发布这个问题的时候,网站上确实没有这个选项。不过现在已经有了。 - undefined
3个回答

28

更新

现在,苹果的开发者网站上包含了WWDC视频的代码片段(至少从2021年开始)。您可以通过在视频播放器下方点击“Code”标签,并向下滚动到“11:59 - 在等待后检查您的假设:一个更好的解决方案”,在视频页面找到“更好的解决方案”的代码。

原文

您可以在开发者应用程序中找到“更好的解决方案”的代码。在开发者应用程序中打开会话,选择“Code”标签,并滚动到“11:59 - 在等待后检查您的假设:一个更好的解决方案”。

screen shot of Developer app

这个截图是从我的iPad上的,但是开发者应用程序也可以在iPhone、Mac和Apple TV上使用。(我不知道Apple TV版本是否提供了查看和复制代码的方式...)
据我所知,无论是在WWDC会议页面上还是作为示例项目的一部分,代码都无法在developer.apple.com网站上找到。
为了后世留存,这是苹果的代码。它与Andy Ibanez的代码非常相似。
actor ImageDownloader {

    private enum CacheEntry {
        case inProgress(Task<Image, Error>)
        case ready(Image)
    }

    private var cache: [URL: CacheEntry] = [:]

    func image(from url: URL) async throws -> Image? {
        if let cached = cache[url] {
            switch cached {
            case .ready(let image):
                return image
            case .inProgress(let task):
                return try await task.value
            }
        }

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

        cache[url] = .inProgress(task)

        do {
            let image = try await task.value
            cache[url] = .ready(image)
            return image
        } catch {
            cache[url] = nil
            throw error
        }
    }
}

10

在我提出原始答案后,我偶然发现了Andy Ibanez的文章,理解 Swift 中并发模型中的 Actors,其中他没有提供苹果的代码,但提供了一些灵感。这个想法非常相似,但他使用枚举来跟踪缓存和挂起的响应:

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

5
关键是保留对“Task”的引用,如果找到,则使用await等待其value
也许:
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
    }
}

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