如何防止actor重入导致重复请求?
How to prevent actor reentrancy resulting in duplicative requests?
在 WWDC 2021 视频中,Protect mutable state with 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 { ... }
}
问题在于 actor 提供可重入性,因此 cache[url, default: image]
引用有效地确保即使您由于某些竞争执行了重复请求,您至少在继续后检查 actor 的缓存,确保您为重复请求获取相同的图像。
在那个视频中,他们 say:
A better solution would be to avoid redundant downloads entirely. We’ve put that solution in the code associated with this video.
但是网站上没有与该视频关联的代码。那么,更好的解决方案是什么?
我了解 actor 重入的好处(如 SE-0306 中所述)。例如,如果下载四张图片,一张不想禁止重入,失去下载的并发性。实际上,我们希望等待对特定图像的重复先验请求的结果(如果有),如果没有,则开始新的 downloadImage
.
关键是保留对 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
}
}
在我想出, I stumbled across Andy Ibanez’s write-up, Understanding Actors in the New Concurrency Model后,他没有提供Apple的代码,而是提供了受其启发的东西。这个想法非常相似,但他使用枚举来跟踪缓存和待处理的响应:
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
}
}
您可以在 the Developer app 中找到“更好的解决方案”代码。在 Developer 应用程序中打开会话,select 代码选项卡,然后滚动到“11:59 - 在等待后检查您的假设:更好的解决方案”。
屏幕截图来自我的 iPad,但 Developer 应用程序也可在 iPhone、Mac 和 Apple TV 上使用。 (我不知道Apple TV版本是否提供了查看和复制代码的方法,但是......)
据我所知,代码在 developer.apple.com 网站上不可用,无论是在 the WWDC session's page 上还是作为示例项目的一部分。
为了后代,这里是 Apple 的代码。它与 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
}
}
}
在 WWDC 2021 视频中,Protect mutable state with 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 { ... }
}
问题在于 actor 提供可重入性,因此 cache[url, default: image]
引用有效地确保即使您由于某些竞争执行了重复请求,您至少在继续后检查 actor 的缓存,确保您为重复请求获取相同的图像。
在那个视频中,他们 say:
A better solution would be to avoid redundant downloads entirely. We’ve put that solution in the code associated with this video.
但是网站上没有与该视频关联的代码。那么,更好的解决方案是什么?
我了解 actor 重入的好处(如 SE-0306 中所述)。例如,如果下载四张图片,一张不想禁止重入,失去下载的并发性。实际上,我们希望等待对特定图像的重复先验请求的结果(如果有),如果没有,则开始新的 downloadImage
.
关键是保留对 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
}
}
在我想出
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
}
}
您可以在 the Developer app 中找到“更好的解决方案”代码。在 Developer 应用程序中打开会话,select 代码选项卡,然后滚动到“11:59 - 在等待后检查您的假设:更好的解决方案”。
屏幕截图来自我的 iPad,但 Developer 应用程序也可在 iPhone、Mac 和 Apple TV 上使用。 (我不知道Apple TV版本是否提供了查看和复制代码的方法,但是......)
据我所知,代码在 developer.apple.com 网站上不可用,无论是在 the WWDC session's page 上还是作为示例项目的一部分。
为了后代,这里是 Apple 的代码。它与 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
}
}
}