为什么要显式调用asyncio.StreamWriter.drain?

时间:2018-12-14 12:37:21

标签: python python-3.x python-asyncio streamwriter

来自文档: https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamWriter.write

  

写入(数据)

Write data to the stream.

This method is not subject to flow control. Calls to write() should be followed by drain().
     

协程流失()

Wait until it is appropriate to resume writing to the stream. Example:

writer.write(data)
await writer.drain()

据我了解,

  • 每次调用drain时,您需要拨打write
  • 如果我猜不到,write将阻塞循环线程

那为什么不写一个自动调用它的协程呢?为什么一个人打个电话write而不必费力?我可以想到两种情况

  1. 您想立即writeclose
  2. 您必须在消息完成之前缓冲一些数据。

第一个是特殊情况,我认为我们可以有一个不同的api。缓冲应在写函数内部处理,应用程序不应在意。


让我提出不同的问题。这样做的缺点是什么? python3.8版本可以有效地做到这一点吗?

async def awrite(writer, data):
    writer.write(data)
    await writer.drain()

注意:drain文档明确声明以下内容:

  

当没有什么可等待的时,drain()立即返回。


再次阅读答案并链接,我认为这些功能是这样工作的。 注意:查看接受的答案以获取更准确的版本。

def write(data):
    remaining = socket.try_write(data)
    if remaining:
        _pendingbuffer.append(remaining) # Buffer will keep growing if other side is slow and we have a lot of data

async def drain():
    if len(_pendingbuffer) < BUF_LIMIT:
        return
    await wait_until_other_side_is_up_to_speed()
    assert len(_pendingbuffer) < BUF_LIMIT

async def awrite(writer, data):
    writer.write(data)
    await writer.drain()        

那么什么时候使用什么:

  1. 当数据不连续时,就像响应HTTP请求一样。我们只需要发送一些数据,而不关心何时到达数据和内存无关紧要-只需使用write
  2. 与上述相同,但需要注意内存,请使用awrite
  3. 将数据流传输到大量客户端(例如某些实时流或巨大的文件)时。如果数据在每个连接的缓冲区中重复,则肯定会导致RAM溢出。在这种情况下,编写一个循环,该循环在每次迭代时都需要处理大量数据,然后调用awrite。如果文件很大,则loop.sendfile(如果可用)更好。

1 个答案:

答案 0 :(得分:5)

  

据我了解,(1)每次调用write时都需要调用rain。 (2)如果我猜不到,写会阻塞循环线程

都不是正确的,但是这种混乱是可以理解的。 write()的工作方式如下:

  • write()的调用只是将数据存储在缓冲区中,而留在事件循环中以便稍后将其实际写出,而无需程序的进一步干预。就应用程序而言,数据在后台写入的速度与另一端能够接收数据的速度一样快。换句话说,每个write()都将调度其数据使用尽可能多的OS级写操作进行传输,并在相应文件描述符实际可写时发出这些写操作。即使没有等待drain(),所有这些都会自动发生。

  • write()不是协程,它绝对从不阻止事件循环。

第二个属性听起来很方便,但实际上是write()的主要缺陷。写入与接受数据是脱钩的,因此,如果您写入数据的速度快于同行读取数据的速度,则内部缓冲区将不断增长,并且您手中将有memory leak。一旦缓冲区过大,等待drain()将暂停协程。每次 写入后,您都不需要等待drain(),但是确实需要偶尔等待,通常是在循环迭代之间。例如:

while True:
    response = await peer1.readline()
    peer2.write(b'<response>')
    peer2.write(response)
    peer2.write(b'</response>')
    await peer2.drain()

drain()如果未决的未写入数据量很小,则会立即返回。如果数据超过高阈值,drain()将暂停调用协程,直到待处理的未写入数据量降至低阈值以下。暂停将导致协程停止从peer1读取数据,这反过来又会导致对等方放慢其向我们发送数据的速度。这种反馈称为背压。

正如异步开发人员reported一样,Python 3.8将支持awrite,从而无需使用显式drain()。 (同时支持added。)

  

缓冲应在写函数内部处理,应用程序不应在意。

这几乎是write()现在的工作方式-它确实处理缓冲,并且它使应用程序不管好坏,不在乎。另请参见this answer,以了解更多信息。


解决问题的编辑部分:

  

再次阅读答案并链接,我认为这些功能的工作原理如下。

write()仍然比这聪明一点。它不会尝试只写入一次,而是会安排数据继续写入,直到没有数据可写入为止。即使您从未等待drain(),也会发生这种情况-应用程序唯一要做的就是让事件循环运行其过程足够长的时间,以将所有内容写出。

更正确的伪代码writedrain可能看起来像这样:

class ToyWriter:
    def __init__(self):
        self._buf = bytearray()
        self._empty = asyncio.Event(True)

    def write(self, data):
        self._buf.extend(data)
        loop.add_writer(self._fd, self._do_write)
        self._empty.clear()

    def _do_write(self):
        # Automatically invoked by the event loop when the
        # file descriptor is writable, regardless of whether
        # anyone calls drain()
        while self._buf:
            try:
                nwritten = os.write(self._fd, self._buf)
            except OSError as e:
                if e.errno == errno.EWOULDBLOCK:
                    return  # continue once we're writable again
                raise
            self._buf = self._buf[nwritten:]
        self._empty.set()
        loop.remove_writer(self._fd, self._do_write)

    async def drain(self):
        if len(self._buf) > 64*1024:
            await self._empty.wait()

    async def awrite(self, data):
        self.write(data)
        await self.drain()

实际的实现更为复杂,因为:

  • 它写在具有自己复杂的Twistedtransport/protocol样式flow control层之上,而不是在os.write之上;
  • 因为drain()并不真正等到缓冲区为空,而是直到到达low watermark为止;
  • EWOULDBLOCK中引发的
  • _do_write以外的异常将在drain()中存储并重新引发。

最后一点是另一个呼叫drain()的充分理由,实际上是由于对等体的写入失败而导致对等体消失。