将asyncio与多处理相结合会出现什么样的问题(如果有的话)?

时间:2014-01-16 10:25:34

标签: python multithreading asynchronous multiprocessing python-asyncio

几乎所有人都知道他们第一次看到Python中的线程时,GIL会让那些真正希望并行处理的人感到痛苦 - 或者至少给它一个机会。

我目前正在研究像Reactor模式这样的实现。实际上我想在一个线程上监听传入的套接字连接,当有人试图连接时,接受该连接并将其传递给另一个线程,以便进行处理。

我(还)确定我可能会遇到什么样的负担。我知道目前设置的传入消息上限为2MB。从理论上讲,我们每秒可以获得数千(虽然我不知道我们是否已经看到过类似的东西)。处理邮件所花费的时间并非非常重要,但显然更快会更好。

我正在调查Reactor模式,并开发了一个使用multiprocessing库的小例子(至少在测试中)似乎工作正常。但是,现在/我们很快就会有asyncio库,它可以为我处理事件循环。

通过合并asynciomultiprocessing

有什么可以咬我的吗?

3 个答案:

答案 0 :(得分:57)

尽管您不应该直接使用asyncio,但您应该可以安全地将multiprocessingmultiprocessing合并而不会有太多麻烦。 asyncio(以及任何其他基于事件循环的异步框架)的主要罪点是阻止事件循环。如果您尝试直接使用multiprocessing,则每当您阻止等待子进程时,您都会阻止事件循环。显然,这很糟糕。

避免这种情况的最简单方法是使用BaseEventLoop.run_in_executor执行concurrent.futures.ProcessPoolExecutor中的函数。 ProcessPoolExecutor是使用multiprocessing.Process实现的进程池,但asyncio内置支持在其中执行函数而不阻塞事件循环。这是一个简单的例子:

import time
import asyncio
from concurrent.futures import ProcessPoolExecutor

def blocking_func(x):
   time.sleep(x) # Pretend this is expensive calculations
   return x * 5

@asyncio.coroutine
def main():
    #pool = multiprocessing.Pool()
    #out = pool.apply(blocking_func, args=(10,)) # This blocks the event loop.
    executor = ProcessPoolExecutor()
    out = yield from loop.run_in_executor(executor, blocking_func, 10)  # This does not
    print(out)

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

对于大多数情况,仅此功能就足够了。如果您发现自己需要multiprocessing中的其他结构,例如QueueEventManager等,则会有一个名为aioprocessing的第三方库(完全披露:我写了它,它提供了asyncio - 所有multiprocessing数据结构的兼容版本。以下是演示:

的示例
import time
import asyncio
import aioprocessing
import multiprocessing

def func(queue, event, lock, items):
    with lock:
        event.set()
        for item in items:
            time.sleep(3)
            queue.put(item+5)
    queue.close()

@asyncio.coroutine
def example(queue, event, lock):
    l = [1,2,3,4,5]
    p = aioprocessing.AioProcess(target=func, args=(queue, event, lock, l)) 
    p.start()
    while True:
        result = yield from queue.coro_get()
        if result is None:
            break
        print("Got result {}".format(result))
    yield from p.coro_join()

@asyncio.coroutine
def example2(queue, event, lock):
    yield from event.coro_wait()
    with (yield from lock):
        yield from queue.coro_put(78)
        yield from queue.coro_put(None) # Shut down the worker

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    queue = aioprocessing.AioQueue()
    lock = aioprocessing.AioLock()
    event = aioprocessing.AioEvent()
    tasks = [ 
        asyncio.async(example(queue, event, lock)),
        asyncio.async(example2(queue, event, lock)),
    ]   
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()

答案 1 :(得分:5)

是的,有很多位可能(或可能不会)咬你。

  • 当您运行类似asyncio的内容时,它希望在一个线程或进程上运行。这不(本身)与并行处理一起使用。你不得不分配工作,同时在单个线程/进程中保留IO操作(特别是那些在套接字上的操作)。
  • 虽然您想要将各个连接切换到不同的处理程序进程很好,但很难实现。第一个障碍是你需要一种方法从asyncio中取出连接而不关闭它。下一个障碍是您不能简单地将文件描述符发送到其他进程,除非您使用来自C扩展的特定于平台(可能是Linux)的代码。
  • 请注意,已知multiprocessing模块可以创建多个用于通信的线程。大多数情况下,当您使用通信结构(例如Queue s)时,会产生一个线程。不幸的是,这些线程并非完全不可见。例如,他们可能无法彻底拆除(当您打算终止您的程序时),但根据其数量,资源使用情况可能会自行显着。

如果您真的打算在个别流程中处理个别关联,我建议您研究不同的方法。例如,您可以将套接字置于侦听模式,然后同时接受来自多个工作进程的连接。一旦工作人员完成处理请求,它就可以接受下一个连接,因此您仍然可以使用比为每个连接分配进程更少的资源。例如,Spamassassin和Apache(mpm prefork)可以使用此工作模型。根据您的使用情况,它可能会更容易,更强大。具体来说,您可以在提供配置数量的请求后让您的工作人员死亡,并由主进程重新生成,从而消除内存泄漏的许多负面影响。

答案 2 :(得分:1)

参见PEP 3156,特别是关于线程交互的部分:

http://www.python.org/dev/peps/pep-3156/#thread-interaction

这清楚地说明了您可能使用的新asyncio方法,包括run_in_executor()。请注意,Executor是在concurrent.futures中定义的,我建议你也看一下。