asyncio任务意外地被推迟

时间:2015-08-08 00:23:59

标签: python python-asyncio

我一直在努力学习asyncio,我有一些意想不到的行为。我已经设置了一个简单的斐波那契服务器,它支持使用流的多个连接。 fib计算是递归写的,所以我可以通过输入大量来模拟长时间运行的计算。正如预期的那样,长时间运行的计算会阻止I / O,直到长时间运行的计算完成。

这是问题所在。我重写了斐波那契函数作为协程。我期望通过从每次递归中产生,控制将回退到事件循环,等待I / O任务将有机会执行,并且您甚至可以同时运行多个fib计算。但情况似乎并非如此。

以下是代码:

import asyncio

@asyncio.coroutine
def fib(n):
    if n < 1:
        return 1
    a = yield from fib(n-1)
    b = yield from fib(n-2)
    return a + b


@asyncio.coroutine
def fib_handler(reader, writer):
    print('Connection from : {}'.format(writer.transport.get_extra_info('peername')))
    while True:
        req = yield from reader.readline()
        if not req:
            break
        print(req)
        n = int(req)
        result = yield from fib(n)
        writer.write('{}\n'.format(result).encode('ascii'))
        yield from writer.drain()
    writer.close()
    print("Closed")


def server(address):
    loop = asyncio.get_event_loop()
    fib_server = asyncio.start_server(fib_handler, *address, loop=loop)
    fib_server = loop.run_until_complete(fib_server)
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        print('closing...')
        fib_server.close()
        loop.run_until_complete(fib_server.wait_closed())
        loop.close()


server(('', 25000))

如果netcat到端口25000并开始输入数字,则此服务器运行良好。但是,如果您开始长时间运行计算(例如35),则在第一次完成之前不会运行任何其他计算。实际上,甚至不会处理其他连接。

我知道事件循环正在反馈递归fib调用的收益,因此控制必须一直下降。但我认为循环将在“trampolining”返回到fib函数之前处理I / O队列中的其他调用(例如生成第二个fib_handler)。

我确信我一定是在误解某些东西,或者有一些我忽略的错误,但我不能为我的生活找到它。

非常感谢您提供的任何见解。

1 个答案:

答案 0 :(得分:3)

第一个问题是您在yield from fib(n)内呼叫fib_handler。包含yield from表示fib_handler将阻止,直到对fib(n)的调用完成,这意味着它无法处理您在fib运行时提供的任何输入。即使您所做的只是fib内的I / O,您也会遇到此问题。要解决此问题,您应该使用asyncio.async(fib(n))(或者最好是asyncio.ensure_future(fib(n)),如果您有足够新的Python版本),可以使用事件循环来安排fib,而不会实际阻止{{1} }}。从那里开始,您可以使用fib_handler将结果写入客户端:

Future.add_done_callback

尽管如此,单凭这一变化仍然无法完全解决问题;虽然它允许多个客户端同时连接和发出命令,但单个客户端仍将获得同步行为。发生这种情况是因为当您直接在协程函数上调用import asyncio from functools import partial from concurrent.futures import ProcessPoolExecutor @asyncio.coroutine def fib(n): if n < 1: return 1 a = yield from fib(n-1) b = yield from fib(n-2) return a + b def do_it(writer, result): writer.write('{}\n'.format(result.result()).encode('ascii')) asyncio.async(writer.drain()) @asyncio.coroutine def fib_handler(reader, writer): print('Connection from : {}'.format(writer.transport.get_extra_info('peername'))) executor = ProcessPoolExecutor(4) loop = asyncio.get_event_loop() while True: req = yield from reader.readline() if not req: break print(req) n = int(req) result = asyncio.async(fib(n)) # Write the result to the client when fib(n) is done. result.add_done_callback(partial(do_it, writer)) writer.close() print("Closed") 时,控件不会返回到事件循环,直到yield from coro()(或coro()调用的另一个协同程序)实际执行某些非阻止I / O.否则,Python将只执行coro而不会产生控制权。这是一个有用的性能优化,因为当你的协程实际上不会阻塞I / O时控制事件循环是浪费时间,特别是考虑到Python的高功能调用开销。

在你的情况下,coro从不进行任何I / O,所以一旦你在fib内部调用yield from fib(n-1),事件循环永远不会再次运行,直到它完成递归,这将阻止fib从客户端读取任何后续输入,直到完成对fib_handler的调用。将所有调用fib中的fib包含在<{1}}中,可确保每次进行asyncio.async调用时都会对事件循环进行控制。当我进行此更改时,除了在yield from asyncio.async(fib(...))中使用asyncio.async(fib(n))之外,我还能够同时处理来自单个客户端的多个输入。这是完整的示例代码:

fib_handler

客户端的输入/输出:

import asyncio
from functools import partial
from concurrent.futures import ProcessPoolExecutor

@asyncio.coroutine
def fib(n):
    if n < 1:
        return 1
    a = yield from fib(n-1)
    b = yield from fib(n-2)
    return a + b

def do_it(writer, result):
    writer.write('{}\n'.format(result.result()).encode('ascii'))
    asyncio.async(writer.drain())

@asyncio.coroutine
def fib_handler(reader, writer):
    print('Connection from : {}'.format(writer.transport.get_extra_info('peername')))
    executor = ProcessPoolExecutor(4)
    loop = asyncio.get_event_loop()
    while True:
        req = yield from reader.readline()
        if not req:
            break
        print(req)
        n = int(req)
        result = asyncio.async(fib(n))
        result.add_done_callback(partial(do_it, writer))
    writer.close()
    print("Closed")

现在,即使这样可行,我也不会使用这个实现,因为它在一个单线程程序中做了一堆CPU绑定的工作,它也希望在同一个线程中提供I / O.这不会很好地扩展,并且不会有理想的性能。相反,我推荐使用dan@dandesk:~$ netcat localhost 25000 35 # This was input 4 # This was input 8 # output 24157817 # output 运行到呼叫loop.run_in_executor在后​​台进程,这使得ASYNCIO线满负荷运行,也使我们能够扩展到{{1来电跨多个核心:

fib