【问题标题】:How to create a limited size cache shared by multiple processes in Python如何在 Python 中创建由多个进程共享的有限大小缓存
【发布时间】:2023-02-24 01:44:13
【问题描述】:

我正在尝试使用由多个进程共享的缓存,使用multiprocessing.Manager's dict。下面的演示给出了一些上下文(来自this answer):

import multiprocessing as mp
import time

def foo_pool(x, cache):
    if x not in cache:
        time.sleep(2)
        cache[x] = x*x
    else:
        print('using cache for', x)
    return cache[x]

result_list = []
def log_result(result):
    result_list.append(result)

def apply_async_with_callback():
    manager = mp.Manager()
    cache = manager.dict()
    pool = mp.Pool()
    jobs = list(range(10)) + list(range(10))
    for i in jobs:
        pool.apply_async(foo_pool, args = (i, cache), callback = log_result)
    pool.close()
    pool.join()
    print(result_list)

if __name__ == '__main__':
    apply_async_with_callback()

运行上面的代码会得到如下结果:

using cache for 0
using cache for 2
using cache for 4
using cache for 1
using cache for 3
using cache for 5
using cache for 7
using cache for 6
[25, 16, 4, 1, 9, 0, 36, 49, 0, 4, 16, 1, 9, 25, 49, 36, 64, 81, 81, 64]

所以缓存按预期工作。

我想要实现的是给这个manager.dict() 一个大小限制,就像functools.lru_cachemaxsize 参数一样。我目前的尝试是:

class LimitedSizeDict:
    def __init__(self, max_size):
        self.max_size = max_size
        self.manager = mp.Manager()
        self.dict = self.manager.dict()
        self.keys = self.manager.list()

    def __getitem__(self, key):
        return self.dict[key]

    def __setitem__(self, key, value):
        if len(self.keys) >= self.max_size:
            oldest_key = self.keys.pop(0)
            del self.dict[oldest_key]
        self.keys.append(key)
        self.dict[key] = value

    def __contains__(self, key):
        return key in self.dict

    def __len__(self):
        return len(self.dict)

    def __iter__(self):
        for key in self.keys:
            yield key

然后使用以下命令启动进程:

def apply_async_with_callback():
    cache = LimitedSizeDict(3)
    pool = mp.Pool()
    jobs = list(range(10)) + list(range(10))
    for i in jobs:
        pool.apply_async(foo_pool, args = (i, cache), callback = log_result)
    pool.close()
    pool.join()
    print(result_list)

但这给了我一个空列表:[]

我想我可能必须继承 multiprocessing.managers.DictProxy 类才能实现这一点,所以我查看了源代码。但是好像没有DictProxy的类定义。

如何给这个共享字典缓存一个大小限制?提前致谢。

【问题讨论】:

    标签: python caching multiprocessing


    【解决方案1】:

    首先,我会定义LimitedSizeDict,这样它就不会与多处理耦合,而是可以成为一个独立的类。因此,它不应该有任何对“管理器”或“被管理对象”的引用。其次,我会为该类定义一个迭代器类,因为您当前的实现是基于生成器的,并且不能跨进程对生成器进行 pickle。第三,有一种方法可以为几乎任何任意类生成代理,如下面的代码所示:

    from multiprocessing import Process
    from multiprocessing.managers import NamespaceProxy, BaseManager
    import inspect
    from collections import deque
    
    class LimitedSizeDict():
        class Iter:
            def __init__(self, cache):
                self._cache = cache
                self._index = 0
    
            def __next__(self):
                if self._index >= len(self._cache):
                    raise StopIteration
                key = self._cache._get_key(self._index)
                self._index += 1
                return key
    
        def __init__(self, max_size):
            self._max_size = max_size
            self._d = {}
            self._keys = deque(maxlen=max_size)
    
        def __len__(self):
            return len(self._keys)
    
        def __getitem__(self, key):
            return self._d[key]
    
        def __setitem__(self, key, value):
            # key may already exist:
            if key not in self._d:
                if len(self._keys) == self._max_size:
                    oldest_key = self._keys[0]
                    del self._d[oldest_key]
                # This automatically will automatically remove self.keys[0]
                self._keys.append(key)
            self._d[key] = value
    
        # Required by iterator:
        def _get_key(self, index):
            return self._keys[index]
    
        def __iter__(self):
            return LimitedSizeDict.Iter(self)
    
    def worker(cache):
        cache['a'] = 1
        cache['b'] = 2
        cache['c'] = 3
        cache['d'] = 4
        for key in cache:
            print(key, cache[key])
    
    
    class ObjProxy(NamespaceProxy):
        """Returns a proxy instance for any user defined data-type. The proxy instance will have the namespace and
        functions of the data-type (except private/protected callables/attributes). Furthermore, the proxy will be
        pickable and can its state can be shared among different processes. """
    
        @classmethod
        def populate_obj_attributes(cls, real_cls):
            DISALLOWED = set(dir(cls))
            DISALLOWED.add('__class__')
            ALLOWED = ['__sizeof__', '__eq__', '__ne__', '__le__', '__repr__', '__dict__', '__lt__',
                       '__gt__']
            new_dict = {}
            for (attr, value) in inspect.getmembers(real_cls, callable):
                if attr not in DISALLOWED or attr in ALLOWED:
                    new_dict[attr] = cls.proxy_wrap(attr)
            return new_dict
    
        @staticmethod
        def proxy_wrap(attr):
            """ This method creates function that calls the proxified object's method."""
            def f(self, *args, **kwargs):
    
                # _callmethod is the method that proxies provided by multiprocessing use to call methods in the proxified object
                return self._callmethod(attr, args, kwargs)
    
            return f
    
    
    # Create a class during runtime
    LimitedSizeDictProxy = type("LimitedSizeDictProxy", (ObjProxy,), ObjProxy.populate_obj_attributes(LimitedSizeDict))
    
    
    if __name__ == '__main__':
        BaseManager.register('LimitedSizeDict', LimitedSizeDict, LimitedSizeDictProxy, exposed=tuple(dir(LimitedSizeDictProxy)))
        with BaseManager() as manager:
            cache = manager.LimitedSizeDict(3)
            p = Process(target=worker, args=(cache,))
            p.start()
            p.join()
    

    印刷:

    b 2
    c 3
    d 4
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-25
      • 1970-01-01
      • 2020-02-18
      • 2021-12-02
      相关资源
      最近更新 更多