【问题标题】:How to prevent actor reentrancy resulting in duplicative requests?如何防止参与者重入导致重复请求?
【发布时间】:2022-01-05 00:30:15
【问题描述】:

在 WWDC 2021 视频中,Protect mutable state with Swift actors,他们提供了以下代码 sn-p:

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, default: image] 参考有效地确保即使您由于某些比赛而执行重复请求,您至少在继续后检查演员的缓存,确保您获得相同的图像重复请求。

在那个视频中,他们say

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

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

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

【问题讨论】:

  • 有什么地方可以投票给“StackOverflow 上最伟大的双重自我回答问题”?我觉得我应该标记这个问题,以便钻石版主可以将其作为示例发布给其他人。
  • 在开发者应用的“代码”标签中查找
  • 是的,正如 Rob Mayoff 在下面的回答中指出的那样,我接受了。它只是在网站上不可用。

标签: swift async-await


【解决方案1】:

您可以在the Developer app 中找到“更好的解决方案”代码。在 Developer 应用中打开会话,选择“代码”选项卡,然后滚动到“11:59 - 在等待后检查您的假设:更好的解决方案”。

屏幕截图来自我的 iPad,但开发者应用程序也可在 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
        }
    }
}

【讨论】:

    【解决方案2】:

    在我提出 my original answer 之后,我偶然发现了 Andy Ibanez 的文章 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
        }
    }
    

    【讨论】:

      【解决方案3】:

      关键是保留对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
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-06-09
        • 1970-01-01
        • 2018-03-31
        • 1970-01-01
        • 2018-11-24
        • 2019-01-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多