0

想象一下有一个函数,它处理繁重的计算工作,我们希望在 Tornado 应用程序上下文中异步执行。此外,我们希望通过将其结果存储到磁盘来懒惰地评估该函数,而不是为相同的参数重新运行该函数两次。

在不缓存结果(记忆)的情况下,可以执行以下操作:

def complex_computation(arguments):
    ...
    return result

@gen.coroutine
def complex_computation_caller(arguments):
    ...
    result = complex_computation(arguments)
    raise gen.Return(result)

假设要实现函数记忆,我们从joblib中选择Memory类。通过简单地用函数装饰函数可以很容易地记住:@mem.cache

@mem.cache
def complex_computation(arguments):
    ...
    return result

哪里mem可以像mem = Memory(cachedir=get_cache_dir()).

现在考虑将两者结合起来,我们在执行器上执行计算复杂的函数:

class TaskRunner(object):
    def __init__(self, loop=None, number_of_workers=1):
        self.executor = futures.ThreadPoolExecutor(number_of_workers)
        self.loop = loop or IOLoop.instance()

    @run_on_executor
    def run(self, func, *args, **kwargs):
        return func(*args, **kwargs)

mem = Memory(cachedir=get_cache_dir())
_runner = TaskRunner(1)

@mem.cache
def complex_computation(arguments):
    ...
    return result

@gen.coroutine
def complex_computation_caller(arguments):
    result = yield _runner.run(complex_computation, arguments)
    ...
    raise gen.Return(result)

那么第一个问题是上述方法在技术上是否正确?

现在让我们考虑以下场景:

@gen.coroutine
def first_coroutine(arguments):
    ...
    result = yield second_coroutine(arguments)
    raise gen.Return(result)

@gen.coroutine
def second_coroutine(arguments):
    ...
    result = yield third_coroutine(arguments)
    raise gen.Return(result)

第二个问题是如何记忆second_coroutine?这样做是否正确:

@gen.coroutine
def first_coroutine(arguments):
    ...
    mem = Memory(cachedir=get_cache_dir())
    mem_second_coroutine = mem(second_coroutine)
    result = yield mem_second_coroutine(arguments)
    raise gen.Return(result)

@gen.coroutine
def second_coroutine(arguments):
    ...
    result = yield third_coroutine(arguments)
    raise gen.Return(result)

[更新 I] 在 Tornado 中缓存和重用函数结果讨论了使用functools.lru_cacheorrepoze.lru.lru_cache作为第二个问题的解决方案。

4

1 回答 1

1

Tornado 协程返回的Future对象是可重用的,因此它通常可以使用内存中的缓存,例如functools.lru_cache,如本问题所述。只要确保将缓存装饰器放在@gen.coroutine.

磁盘缓存(似乎由 的cachedir参数暗示Memory)更棘手,因为Future对象通常不能写入磁盘。您的TaskRunner示例应该可以工作,但它与其他示例完全不同,因为complex_calculation它不是协程。您的最后一个示例将不起作用,因为它试图将Future对象放入缓存中。

相反,如果你想用装饰器缓存东西,你需要一个装饰器,用第二个协程包装内部协程。像这样的东西:

def cached_coroutine(f):
    @gen.coroutine
    def wrapped(*args):
        if args in cache:
            return cache[args]
        result = yield f(*args)
        cache[args] = f
        return result
    return wrapped
于 2016-04-18T03:11:06.357 回答