[Python] subprocess.Popen 으로 stdout, stderr non-blocking 으로 결과 가져오기 Develop Tip

다음과 같은 파이썬 샘플이 있습니다. (po_callee.py)

import sys
import time
import datetime
import argparse


################################################################################
def do(args):
    for i in range(args.loop):
        if 0 < i and args.stderr > 0 and i % args.stderr == 0:
            msg = '[%s] message stderr [%d]\n' % (datetime.datetime.now(), i)
            sys.stderr.write(msg)
        else:
            msg = '[%s] message stdout [%d]\n' % (datetime.datetime.now(), i)
            sys.stdout.write(msg)
        time.sleep(1)


################################################################################
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Popen callee test program')
    parser.add_argument('--loop', type=int,
                        default=30,
                        help='loop for the test, default is 30')
    parser.add_argument('--stderr', type=int,
                        default=10,
                        help='every other n print stdout, default is 10. 0 means no stderr')
    _args = parser.parse_args()
    do(_args)

이 프로그램은 다음과 같이 30개의 결과를 출력하는데 

[2019-06-25 11:31:14.465098] message stdout [0]
[2019-06-25 11:31:15.476131] message stdout [1]
[2019-06-25 11:31:16.483655] message stdout [2]
[2019-06-25 11:31:17.490693] message stdout [3]
[2019-06-25 11:31:18.498152] message stdout [4]
[2019-06-25 11:31:19.505642] message stdout [5]
[2019-06-25 11:31:20.513192] message stdout [6]
[2019-06-25 11:31:21.520630] message stdout [7]
[2019-06-25 11:31:22.527671] message stdout [8]
[2019-06-25 11:31:23.535174] message stdout [9]
[2019-06-25 11:31:24.542701] message stderr [10]
[2019-06-25 11:31:25.550154] message stdout [11]
[2019-06-25 11:31:26.557651] message stdout [12]
[2019-06-25 11:31:27.565560] message stdout [13]
[2019-06-25 11:31:28.572739] message stdout [14]
[2019-06-25 11:31:29.579713] message stdout [15]
[2019-06-25 11:31:30.587164] message stdout [16]
[2019-06-25 11:31:31.594650] message stdout [17]
[2019-06-25 11:31:32.602148] message stdout [18]
[2019-06-25 11:31:33.609651] message stdout [19]
[2019-06-25 11:31:34.617144] message stderr [20]
[2019-06-25 11:31:35.624593] message stdout [21]
[2019-06-25 11:31:36.631692] message stdout [22]
[2019-06-25 11:31:37.639128] message stdout [23]
[2019-06-25 11:31:38.647124] message stdout [24]
[2019-06-25 11:31:39.654169] message stdout [25]
[2019-06-25 11:31:40.662560] message stdout [26]
[2019-06-25 11:31:41.669633] message stdout [27]
[2019-06-25 11:31:42.679560] message stdout [28]
[2019-06-25 11:31:43.684141] message stdout [29]

그 중에 2개의 stderr 출력이 있습니다.

그런데 다른 파이썬 스크립트에서 위의 python을 호출하는데,

일반적으로는

import subprocess
po = Popen([sys.executable, 'po_callee.py'], stdout=PIPE, stderr=PIPE)
print(po.stdout.read())
print(po.stderr.read())

와 같이 하는데 이 때에는 po.stdout.read() 에서 모든 결과를 다 읽을 때까지
(해당 스트림이 닫히기 전까지) 기다립니다.

해당 read() 대신, readline() 등등도 해 보았고,
결국 select 등을 사용하여 보았지만,
윈도우에서는 제대로 지원안되는 단점이 존재합니다.
pipe를 써서 된다고도 누군가는 그랬는데, 그것도 실패했습니다.

결국 이를 다음과 같이 해결하였습니다.


import sys
import time
from subprocess import PIPE, Popen
from threading import Thread
from queue import Queue, Empty

ON_POSIX = 'posix' in sys.builtin_module_names


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


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


################################################################################
def do():
    po = Popen([sys.executable, 'po_callee.py'],
              stdout=PIPE, stderr=PIPE,
              bufsize=1, close_fds=ON_POSIX)
    q_out, q_err = Queue(), Queue()
    t_out = Thread(target=enqueue_stdout, args=(po.stdout, q_out))
    t_out.daemon = True  # thread dies with the program
    t_out.start()
    t_err = Thread(target=enqueue_stderr, args=(po.stderr, q_err))
    t_err.daemon = True  # thread dies with the program
    t_err.start()

    while po.poll() is None:
        try:
            line = q_out.get_nowait()
            if line:
                print(line.decode('utf-8').rstrip())
        except Empty:
            pass
        try:
            line = q_err.get_nowait()  # or q.get(timeout=.1)
            if line:
                sys.stderr.write('%s\n' % line.decode('utf-8').rstrip())
        except Empty:
            pass
        time.sleep(1)


################################################################################
if __name__ == '__main__':
    do()

위의 것은 파이썬 쓰레드를 두 개 만들어,
하나는 stdout, 다른 하나는 stderr 용으로 만든 다음, 이것을 해당 Queue에 넣습니다.
그리고 Q에서 get_nowait() 로 non-blocking 으로 결과 값을 가져와서 처리하는 것입니다.

GIL 때문에 파이썬 쓰레드를 사용하지 말라고 하지만, 이것은 어디까지나 CPU bound job인 경우에 해당하므로,
위와 같이 stdout, stderr 로 출력하는 것은 해당되지 않습니다.

여러번 해당 내용을 제대로 해 봐야지 했었는데, 완벽한 방법은 아니더라도,
현재 이렇게 해결하였습니다.


어느 분께는 도움이 되셨기를 ..





핑백

덧글

  • 지훈현서아빠 2019/06/25 14:34 # 답글

    그런데 1초만에 끝나거나 더 출력한 것이 있었다면 "while po.poll() is None:" loop 이 끝난 다음 blocking 으로 더 가져오는 코드를 넣어야 합니다..
  • 202 2019/06/26 09:46 # 삭제 답글

    아 비슷하게 찾던게 있었는데 도움 되었습니다.
  • 지훈현서아빠 2019/06/26 15:57 #

    도움이 되셨다면 저의 보람입니다~ ^^
댓글 입력 영역

구글애드텍스트