是否可以同时读取多个异步流?

时间:2020-09-14 15:17:14

标签: python select stream python-asyncio

我需要读取同时运行的几个异步任务的输出。

这些任务实际上是使用asyncio.create_subprocess_exec()创建的。

以最简单的形式,我需要打印单个进程的stdout / stderr,同时在单独的字符串中累积行。

我当前的(有效)代码是:

async def run_command(*args, stdin=None, can_fail=False, echo=False):
    """
    Run command asynchronously in subprocess.

    Waits for command completion and returns return code, stdout and stdin

    Example from:
        http://asyncio.readthedocs.io/en/latest/subprocess.html
    """
    # Create subprocess
    try:
        process = await asyncio.create_subprocess_exec(
            *args,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
    except (FileNotFoundError, OSError):
        if not can_fail:
            log.error("run_command(%s): Error FileNotFound", args)
        return -1, '', 'File "%s" NotFound' % args[0]

    # Status
    log.debug("run_command(%s): pid=%s", args, process.pid)

    # Wait for the subprocess to finish
    stdout, stderr = await process.communicate(stdin)

    # Progress
    if process.returncode == 0:
        log.debug("run_command(%s): ok: %s", process.pid, stdout.decode().strip())
    else:
        log.debug("run_command(%s): ko: %s", process.pid, stderr.decode().strip())

    # Result
    result = process.returncode, stdout.decode().strip(), stderr.decode().strip()

    # Return stdout
    return result

此代码的问题是,直到进程终止,我什么都看不到。一些产生的过程可能需要几分钟才能完成,并且在执行时会打印“有趣的”信息。捕获时如何立即打印(或记录)输出? (我知道省略捕获会打印出基本过程,但我也需要捕获)

我试图做一些事情:

_stdout = ''
while True:
    data = process.stdout.readline()
    if not data:
        break
    print(data)
    _stdout += data.decode()

但是我不知道如何将其扩展到多个流(在这种情况下,仅是stdout / stderr,但是可能扩展到多个程序)。是否有类似于select()调用的内容?

任何提示欢迎

1 个答案:

答案 0 :(得分:1)

是否有类似于select()通话的内容?

答案必须是肯定的,因为asyncio完全基于对select()的调用而建立。但是,如何将其转换为流级别的选择并不总是很明显。需要注意的是,您不应该尝试完全选择流-而是开始阅读流并依靠选择协程进展的能力。因此,select()的等效项是使用asyncio.wait(return_when=FIRST_COMPLETED)来循环驱动读取。

一个更优雅的选择是在每个任务都执行其任务的情况下生成单独的任务,并让它们并行运行。与使用select相比,该代码更易于理解,可以简化为一次对gather的调用,但是在异步情况下,asyncio完全执行了所请求的select()的类型:

import asyncio, sys, io

async def _read_all(stream, echo):
    # helper function to read the whole stream, optionally
    # displaying data as it arrives
    buf = io.BytesIO()  # BytesIO is preferred to +=
    while True:
        chunk = await stream.read(4096)
        if len(chunk) == 0:
            break
        buf.write(chunk)
        if echo:
            sys.stdout.buffer.write(chunk)
    return buf.getvalue()

async def run_command(*args, stdin=None, echo=False):
    process = await asyncio.create_subprocess_exec(
        *args,
        stdin=asyncio.subprocess.PIPE,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE
    )
    if stdin is not None:
        process.stdin.write(stdin)
    process.stdin.close()
    stdout, stderr = await asyncio.gather(
        _read_all(process.stdout, echo),
        _read_all(process.stderr, echo)
    )
    return process.returncode, stdout.decode().strip(), stderr.decode().strip()

请注意,asyncio的write()不是协程,它默认是在后台编写,因此我们不需要在gather()的协程中包括该写操作。