立即预取项目和 return 个请求的项目

Prefetch items and return requested item immediately

我需要从网络共享加载大量图像数据进行处理(速度不是很快)。图像按照顺序命名(例如 1.png、2.png、3.png 等)。

在大多数情况下,加载将按此顺序进行(在 n.png 之后加载 n+1.png)。我想在实际请求之前在内存中有 n+1.png。

我想(也)保留一个缓存,这样返回 1 个图像不需要磁盘访问。

我设想的是这样的:

  1. 请求索引为 n 的图片
  2. 检查n.png是否在缓存中,如果图片不在缓存中: 一种。从磁盘加载图像 b.将图像放入缓存
  3. 对索引为 n+1 的图像执行步骤 1 和 2
  4. 不要等待第 3 步完成,而是从缓存中获取图像并return该图像

很高兴有这样的功能:在后台清理缓存,使其只包含最后请求的 10 个项目,或者它删除第一个请求的项目,直到它包含最大值。 10 项(我可以想象后一种选择更容易实施,同时对我的情况来说已经足够好了)。

我正在使用 Python 3.5。我正在使用 PyQt5,但我更喜欢该函数不依赖于 PyQt5 功能(但如果这使得实现更多 clean/easy/readable 我会使用它)。

简单的答案(假设您没有使用协程或类似程序,您可能不知道您使用的是 PyQt5)是生成一个守护进程后台线程以将图像 n+1 加载到缓存中.像这样:

def load(self, n):
    with self._cache_lock:
        try:
            return self._cache[n]
        except KeyError:
            self._cache[n] = self._load_image(n)
            return self._cache[n]
def process_image(self, n):
    img = self.load(n)
    threading.Thread(target=partial(self.load, n+1), daemon=True).start()
    self.step1(img)
    self.step2(img)

此设计的问题在于您在整个 _load 操作周围持有锁。如果 step1step2_load_image 花费的时间长得多,通过允许罕见的重复工作来避免该锁定可能更便宜:

def cacheget(self, n):
    with self._cache_lock:
        return self._cache.get(n)
def preload(self, n):
    img = self._load_image(n)
    with self._cache_lock:
        self._cache[n] = img
    return img
def process_image(self, n):
    img = self.cacheget(n)
    if img is None:
        img = self.preload(n)
    threading.Thread(target=partial(self.preload, n+1), daemon=True).start()
    self.step1(img)
    self.step2(img)

如果您希望并行执行大量处理,您可能希望使用 ThreadPoolExecutor 来排队所有预加载,而不是为每个预加载一个守护线程。

如果要清除旧的缓存值,请参阅 lru_cache and its implementation。有很多调整决定要做(比如:你真的想要后台缓存垃圾收集,还是你可以像 lru_cache 那样在添加第 10 个项目时只推出最旧的项目?),但是 none 一旦你决定了你想要什么,这些选项就特别难以构建。

我开发了一个 library,我相信它确实可以做到这一点:

files = ['1.png', '2.png', '3.png']
images = seqtools.smap(load_img, files)  # (on-demand) image loading
images = seqtools.prefetch(images, max_buffered=10, method='thread')  # prefetching
images = seqtools.add_cache(images, cache_size=10)  # lru cache
image[0]
images[1]

预取的工作方式是将紧接在最后一个请求之后的项目的评估加入队列。

Thread/process 并自动处理内存清理。

请注意,上面的代码在内存中有 20 张图像:10 张用于预取缓冲区,10 张用于最近请求的项目。

最后,您可能希望根据加载函数的作用在线程后端和多处理后端之间切换。