Python:从多个子进程异步打印stdout

时间:2014-03-21 17:30:18

标签: python windows multithreading python-2.7

我正在测试一种从Python 2.7中的几个子进程打印出stdout的方法。我所设置的是一个主要过程,目前产生三个子过程并吐出它们的输出。每个子进程都是一个for循环,它会在一段随机的时间内进入休眠状态,当它唤醒时,会说“睡眠时间为X秒”。

我看到的问题是打印出来似乎是同步的。假设子进程A休眠1秒,子进程B休眠3秒,子进程C休眠10秒。当它试图查看子进程C是否有某些东西时,主进程会停止整整10秒,即使其他两个可能已经睡眠并打印出来。这是为了模拟子进程是否真的没有任何东西输出比其他两个更长的时间。

我需要一个适用于Windows的解决方案。

我的代码如下:

main_process.py

import sys
import subprocess

logfile = open('logfile.txt', 'w')
processes = [
            subprocess.Popen('python subproc_1.py', stdout=subprocess.PIPE, bufsize=1), 
            subprocess.Popen('python subproc_2.py', stdout=subprocess.PIPE, bufsize=1), 
            subprocess.Popen('python subproc_3.py', stdout=subprocess.PIPE, bufsize=1), 
        ]


while True:
    line = processes[0].stdout.readline() 
    if line != '':
        sys.stdout.write(line)
        logfile.write(line)

    line = processes[1].stdout.readline()
    if line != '':
        sys.stdout.write(line)
        logfile.write(line)

    line = processes[2].stdout.readline()
    if line != '':
        sys.stdout.write(line)
        logfile.write(line)

    #If everyone is dead, break
    if processes[0].poll() is not None and \
       processes[1].poll() is not None and \
       processes[2].poll() is not None:
        break

processes[0].wait()
processes[1].wait()

print 'Done'

subproc_1.py/subproc_2.py/subproc_3.py

import time, sys, random

sleep_time = random.random() * 3
for x in range(0, 20):
    print "[PROC1] Slept for {0} seconds".format(sleep_time)
    sys.stdout.flush()
    time.sleep(sleep_time)
    sleep_time = random.random() * 3 #this is different for each subprocess.

更新:解决方案

将以下答案与this question一起使用,这应该可行。

import sys
import subprocess
from threading import Thread

try:
    from Queue import Queue, Empty
except ImportError:
    from queue import Queue, Empty # for Python 3.x

ON_POSIX = 'posix' in sys.builtin_module_names

def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        queue.put(line)
    out.close()

if __name__ == '__main__':
    logfile = open('logfile.txt', 'w')
    processes = [
                subprocess.Popen('python subproc_1.py', stdout=subprocess.PIPE, bufsize=1), 
                subprocess.Popen('python subproc_2.py', stdout=subprocess.PIPE, bufsize=1), 
                subprocess.Popen('python subproc_3.py', stdout=subprocess.PIPE, bufsize=1), 
            ]
    q = Queue()
    threads = []
    for p in processes:
        threads.append(Thread(target=enqueue_output, args=(p.stdout, q)))

    for t in threads:
        t.daemon = True
        t.start()

    while True:
        try:
            line = q.get_nowait()
        except Empty:
            pass
        else:
            sys.stdout.write(line)
            logfile.write(line)
            logfile.flush()

        #break when all processes are done.
        if all(p.poll() is not None for p in processes):
            break

    print 'All processes done'

我不确定在while循环结束时是否需要任何清理代码。如果有人对此有任何意见,请添加它们。

每个子脚本看起来都与此类似(我为了更好的例子而编辑):

import datetime, time, sys, random

for x in range(0, 20):
    sleep_time = random.random() * 3
    time.sleep(sleep_time)
    timestamp = datetime.datetime.fromtimestamp(time.time()).strftime('%H%M%S.%f')
    print "[{0}][PROC1] Slept for {1} seconds".format(timestamp, sleep_time)
    sys.stdout.flush()

print "[{0}][PROC1] Done".format(timestamp)
sys.stdout.flush()

1 个答案:

答案 0 :(得分:2)

你的问题来自readline()是阻塞功能的事实;如果你在文件对象上调用它并且没有等待读取的行,则调用将不会返回,直到有一行输出。所以你现在所拥有的将按顺序从子进程1,2和3 中重复读取,暂停,直到输出就绪。

编辑:OP澄清说他们在Windows上,这使得以下不适用。)

如果要从准备好的输出流中读取,则需要使用select模块以非阻塞方式检查流的状态,然后尝试仅读取已准备好的流。 select提供了各种方法,但为了示例,我们将使用select.select()。启动子流程后,您将拥有以下内容:

streams = [p.stdout for p in processes]

def output(s):
    for f in [sys.stdout, logfile]:
        f.write(s)
        f.flush()

while True:
    rstreams, _, _ = select.select(streams, [], [])
    for stream in rstreams:
        line = stream.readline()
        output(line)
    if all(p.poll() is not None for p in processes):
        break

for stream in streams:
    output(stream.read())

当使用三个文件对象(或文件描述符)列表调用时,select()的作用是返回其参数的三个子集,即准备好读取的流,可以写入的流,或者具有错误的情况。因此,在循环的每次迭代中,我们检查哪些输出流已准备好读取,并迭代这些输出流。然后我们重复一遍(请注意,您在此处对输出进行行缓冲非常重要;上面的代码假定如果流已准备好进行读取,则至少有一个完整的行准备好被读取。你指定不同的缓冲,上面可以阻止。)

原始代码的另一个问题:当poll()报告所有已退出的子进程后退出循环时,您可能没有读取所有输出。因此,您需要对流进行最后一次扫描以读取任何剩余输出。

注意:我给出的示例代码并没有尝试那么难以捕获子进程'输出完全按其可用的顺序输出(这是不可能完美的,但可以比上面设计的更近似地进行近似)。它还缺少其他改进(例如,在主循环中它将继续在每个子进程的stdout上选择,即使在一些已经终止之后,这是无害的,但是效率低下)。它只是为了说明非阻塞IO的基本技术。