如何缓存asyncio协同程序

时间:2015-12-06 11:32:16

标签: python-3.x python-asyncio

我正在使用aiohttp在python 3.4中创建一个简单的HTTP请求,如下所示:

response = yield from aiohttp.get(url)

应用程序一遍又一遍地请求相同的URL,所以我自然想要缓存它。我的第一次尝试是这样的:

@functools.lru_cache(maxsize=128)
def cached_request(url):
    return aiohttp.get(url)

第一次调用cached_request可以正常工作,但在以后的调用中,我最终会使用None而不是响应对象。

我对asyncio很陌生,所以我尝试了asyncio.coroutine装饰器,yield from和其他一些东西的很多组合,但似乎都没有。

那么缓存协同程序是如何工作的?

9 个答案:

答案 0 :(得分:3)

我自己写了一个简单的缓存装饰器:

def async_cache(maxsize=128):
    cache = {}

    def decorator(fn):
        def wrapper(*args):                                                         
            key = ':'.join(args)

            if key not in cache:
                if len(cache) >= maxsize:
                    del cache[cache.keys().next()]

                cache[key] = yield from fn(*args)

            return cache[key]

        return wrapper

    return decorator


@async_cache()
@asyncio.coroutine
def expensive_io():
    ....

这种工作方式。但是很多方面都可能得到改善。例如:如果在第一次调用返回之前第二次调用缓存函数,它将再次执行。

答案 1 :(得分:3)

也许有点晚了,但我已经开始了一个可能有用的新软件包:https://github.com/argaen/aiocache。我们随时欢迎您的贡献/评论。

一个例子:

import asyncio

from collections import namedtuple

from aiocache import cached
from aiocache.serializers import PickleSerializer

Result = namedtuple('Result', "content, status")


@cached(ttl=10, serializer=PickleSerializer())
async def async_main():
    print("First ASYNC non cached call...")
    await asyncio.sleep(1)
    return Result("content", 200)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    print(loop.run_until_complete(async_main()))
    print(loop.run_until_complete(async_main()))
    print(loop.run_until_complete(async_main()))
    print(loop.run_until_complete(async_main()))

请注意,作为额外的,它可以使用Pickle序列化将任何python对象缓存到redis中。如果您只想使用内存,可以使用SimpleMemoryCache后端:)。

答案 2 :(得分:2)

要将functools.lru_cache与协同程序一起使用,以下代码可以正常运行。

class Cacheable:
    def __init__(self, co):
        self.co = co
        self.done = False
        self.result = None
        self.lock = asyncio.Lock()

    def __await__(self):
        with (yield from self.lock):
            if self.done:
                return self.result
            self.result = yield from self.co.__await__()
            self.done = True
            return self.result

def cacheable(f):
    def wrapped(*args, **kwargs):
        r = f(*args, **kwargs)
        return Cacheable(r)
    return wrapped


@functools.lru_cache()
@cacheable
async def foo():
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.text()

以下是线程安全的

class ThreadSafeCacheable:
    def __init__(self, co):
        self.co = co
        self.done = False
        self.result = None
        self.lock = threading.Lock()

    def __await__(self):
        while True:
            if self.done:
                return self.result
            if self.lock.acquire(blocking=False):
                self.result = yield from self.co.__await__()
                self.done = True
                return self.result
            else:
                yield from asyncio.sleep(0.005)

答案 3 :(得分:0)

我对aiohttp并不熟悉所以我不确定会发生什么会导致Nones被返回,但是lru_cache装饰器不能用于异步函数。

我使用的装饰器基本上是一样的;请注意,与上面的tobib的装饰器不同,它总是会返回一个未来或任务,而不是值:

from collections import OrderedDict
from functools import _make_key, wraps

def future_lru_cache(maxsize=128):
    # support use as decorator without calling, for this case maxsize will
    # not be an int
    try:
        real_max_size = int(maxsize)
    except ValueError:
        real_max_size = 128

    cache = OrderedDict()

    async def run_and_cache(func, args, kwargs):
        """Run func with the specified arguments and store the result
        in cache."""
        result = await func(*args, **kwargs)
        cache[_make_key(args, kwargs, False)] = result
        if len(cache) > real_max_size:
            cache.popitem(False)
        return result

    def wrapper(func):
        @wraps(func)
        def decorator(*args, **kwargs):
            key = _make_key(args, kwargs, False)
            if key in cache:
                # Some protection against duplicating calls already in
                # progress: when starting the call cache the future, and if
                # the same thing is requested again return that future.
                if isinstance(cache[key], asyncio.Future):
                    return cache[key]
                else:
                    f = asyncio.Future()
                    f.set_result(cache[key])
                    return f
            else:
                task = asyncio.Task(run_and_cache(func, args, kwargs))
                cache[key] = task
                return task
        return decorator

    if callable(maxsize):
        return wrapper(maxsize)
    else:
        return wrapper

我使用functools中的_make_key作为lru_cache,我猜它应该是私有的,所以最好把它复制一遍。

答案 4 :(得分:0)

lru decorator的另一个变种,它缓存尚未完成的协同程序,对同一个密钥的并行请求非常有用:

_KEY

答案 5 :(得分:0)

我认为最简单的方法是使用aiohttp_cachedocumentation

pip install aiohttp-cache

并在代码中使用它:

from aiohttp_cache import cache, setup_cache

@cache()  # <-- DECORATED FUNCTION
async def example_1(request):
    return web.Response(text="Example")


app = web.Application()

app.router.add_route('GET', "/", example_1)

setup_cache(app)  # <-- INITIALIZED aiohttp-cache

web.run_app(app, host="127.0.0.1")

答案 6 :(得分:0)

此处存在lru_cache的一个流行的异步版本:async_lru

答案 7 :(得分:0)

尝试使用async-cache :pypi async-cache :github在python中缓存异步功能。

它还支持具有 user defined object type unhashable 类型参数的函数 functools.lru_cache async_lru 不支持。

用法:

pip install async-cache
from async-cache import AsyncLRU

@AsyncLRU(maxsize=128)
async def func(*args, **kwargs):
    pass

答案 8 :(得分:0)

这是我认为最容易完成的方法,使用内置的 lru_cache 和期货:

import asyncio
import functools

# parameterless decorator
def async_lru_cache_decorator(async_function):
    @functools.lru_cache
    def cached_async_function(*args, **kwargs):
        coroutine = async_function(*args, **kwargs)
        return asyncio.ensure_future(coroutine)
    return cached_async_function

# decorator with options
def async_lru_cache(*lru_cache_args, **lru_cache_kwargs):
    def async_lru_cache_decorator(async_function):
        @functools.lru_cache(*lru_cache_args, **lru_cache_kwargs)
        def cached_async_function(*args, **kwargs):
            coroutine = async_function(*args, **kwargs)
            return asyncio.ensure_future(coroutine)
        return cached_async_function
    return async_lru_cache_decorator

@async_lru_cache(maxsize=128)
async def your_async_function(...): ...

这基本上是获取您的原始函数并将其包装起来,以便我可以存储它返回的 Coroutine 并将其转换为 Future。这样,这可以被视为常规函数,您可以像往常一样lru_cache-它。

为什么需要将它包装在 Future 中? Python 协程是低级构造,您不能多次await(您会得到 RuntimeError: cannot reuse already awaited coroutine)。另一方面,期货很方便,可以连续等待并返回相同的结果。

需要注意的是,缓存 Future 也会在原始函数引发 Error 时进行缓存。原始 lru_cache 不会缓存中断的执行,因此请注意使用上述解决方案的这种边缘情况。

可以进行进一步调整以合并无参数装饰器和参数化装饰器,例如支持这两种用法的原始 lru_cache