异步IO的性能表现

11

我正在尝试熟悉asyncio,因此我决定编写一个数据库客户端。然而,性能与同步代码完全相同。我确信这是我的某个概念误解。能否有人解释一下我做错了什么?

请参见以下代码示例:

class Connection:
    def __init__(self, reader, writer, loop):
        self.futures = deque()

        # ...

        self.reader_task = asyncio.async(self.recv_data(), loop=self.loop)

    @asyncio.coroutine
    def recv_data(self):
        while 1:
            try:
                response = yield from self.reader.readexactly(4)
                size, = struct.unpack('I', response)
                response = yield from self.reader.readexactly(size)

                # ...                

                future = self.futures.popleft()

                if not future.cancelled():
                    future.set_result(response)

            except Exception:
                break

    def send_data(self, data):
        future = asyncio.Future(loop=self.loop)
        self.futures.append(future)

        self.writer.write(data)

        return future


loop = asyncio.get_event_loop()


@asyncio.coroutine
def benchmark():
    connection = yield from create_connection(loop=loop, ...)

    for i in range(10000):
        yield from connection.send_data(...)


s = time.monotonic()

loop.run_until_complete(benchmark())

e = time.monotonic()
print('Requests per second:', int(10000 / (e - s)))

提前致谢。

2个回答

13

在调用 send_data 方法时出现了错误。目前的代码如下:

@asyncio.coroutine
def benchmark():
    connection = yield from create_connection(loop=loop, ...)

    for i in range(10000):
        yield from connection.send_data(...)

通过在for循环中使用yield from,您正在等待从send_data返回的future产生结果,然后再继续下一个调用。这使得您的程序基本上是同步的。您想要对所有对send_data的调用完成后,再等待结果:

@asyncio.coroutine
def benchmark():
    connection = yield from create_connection(loop=loop, ...)
    yield from asyncio.wait([connection.send_data(..) for _ in range(10000)])

太好了,谢谢。从我理解的来看,这与为每个“send_data”调用创建一个任务是相同的吗? - Andrew
2
@Andrew 大体上是这样,不过你仍需要在“benchmark”中添加代码来等待每个“Task”完成。实际上,我相信调用“asyncio.wait”将会在内部将所有传递给它的协程对象转换为“Task”实例。 - dano
是的,你们两个都是正确的。asyncio.wait将包装任何传递进来的协程对象或可等待对象到一个Task未来对象中。自己使用loop.create_taskasyncio.ensure_future进行包装可能会在循环中安排它们,但不会阻止协程代码的执行,直到它们最终完成。你仍然需要yield from这些Task或将它们传递给像asyncio.wait这样的东西。 - Justin Turner Arthur

2
Python的asyncio模块是单线程的:
这个模块提供了基础设施,用于使用协程编写单线程并发代码,通过多路复用I/O访问套接字和其他资源,运行网络客户端和服务器以及其他相关原语。 这个问题解释了为什么asyncio可能比线程慢,但简而言之:asyncio使用单个线程来执行您的代码,因此即使您有多个协程,它们也会按顺序执行。线程池用于执行一些回调和I/O。由于GIL,线程也按顺序执行用户代码,尽管I/O操作可以同步运行。
使用asyncio不会使您的代码比串行执行的代码更快,因为事件循环一次只能运行一个协程。

6
OP的代码仍然比同步代码运行得更快,因为它受到I/O限制。单线程并不重要——当一个协程中有I/O正在运行时,其他协程可以执行。你引用的那个问题是一个比较特殊的情况——它使用了getaddrinfo,而这实际上并没有使用异步I/O来实现。它使用了一个小的ThreadPool来限制可用的并行性。这使它比常规的多线程代码慢,但仍然比同步代码快,这也是这个问题所涉及的。 - dano
1
@dano 那是我的错误。我没有理解得足够清楚。我会投票支持你的回答。 - zstewart
1
没问题。异步框架是一个相当奇怪的概念,需要花费一些时间来理解。你在回答中写的最后一句话实际上基本正确,但这是由于编码错误而不是asyncio的限制导致的。 - dano

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接