subprocessでのデッドロックらしきものを回避する

Pythonのプログラムの中で、外部プログラムを実行するのにpopen2を使っていたのだが、さすがに古いモジュールなのでそろそろsubprocessモジュールに置き換えるべきかな、と思っていた矢先に今回の問題が発生した。

問題

数値計算を行うプログラムを作成して、gccコンパイルした。プログラムの実行環境はWindows。プログラムの実行方法は、パラメータを与えると処理結果を返す、というものだが、パラメータの与え方はインタラクティブにしている。一気にパラメータを与えるのではなく、一つ与えては少しの処理を行い途中経過を出力し、また一つ与えては経過を出力する、という形式になっている。
つまり、標準入力へのデータの送信と、標準出力への結果の出力が交互に繰り返されるプログラムだ。

このプログラムの実行をPythonコードの中でサブプロセスを生成することで行う。問題が生じたPythonコードは以下の通り。

import os
import subprocess

def exe_cmd(cmd_name, inputs):
    args = [cmd_name]
    p = subprocess.Popen(args, 
                         shell=True,
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                        )
    stdout = p.stdout
    stdin = p.stdin
    stderr = p.stderr

    for x in inputs:
        stdin.write(x)
    if inputs:
        try:
            stdin.flush()
        except:
            success = False

    while True:
        s = stdout.readline()
        print s
        if s == '':
            break

    s = stderr.read()
    print 'error:', s

    return True

if __name__ == '__main__':
    cmd_name = 'prog'
    inputs = list()
    inputs.append('0\n')
    inputs.append('1\n')
  inputs.append('2\n')
 
    exe_cmd(cmd_name, inputs)

こんな感じ。
ここでは、外部プログラムprogに渡している入力は0と1と2だけだが、一つ数値を渡すたびにprogは結果を標準出力に書き出す。これで動くときと動かないときがある、という問題が生じた。

動くときと動かないときの違いは、progを何でコンパイルしたか、ということで生じることが分かった。具体的には、CygwinMinGWコンパイルしたモジュールで違いが生じる。コマンドプロンプトで実行すると、両者のモジュールで挙動の差は生じない。サブプロセスとして実行したときにだけ挙動が異なる。変なの。

問題が発生する外部プログラムのパターン

  • 入力→出力→入力→出力→(繰り返し)→終了 :この場合問題が発生
  • 入力→出力→終了 :こういうプログラムだと問題なし

解決方法(communicateメソッドを使う)

動かないときというのは、標準出力へ書き出される結果を読みとるところ。上のコードの該当箇所は、

s = stdout.readline()

で処理が止まる。フリーズする。コンパイラが悪いのか、とも考えたが、独立に実行した場合には問題ないことを考えると、サブプロセスとしての呼び出し方法に問題があるのではないかと、いろいろ調べた。

この場合、サブプロセスとして実行されるプログラムへの書き込みと書き出しが交互に行われていることからデッドロックが疑われる。つまり子プロセスとの通信でデッドロックが起こっているのではないかと想像した。

参考サイト:

上のサイトでsubprocessの概要を確認して、下のサイトの実装例に倣ってPythonコードの関数部分を変えてみた。

def exe_cmd(cmd_name, inputs):
    args = [cmd_path]
    p = subprocess.Popen(args, 
                         shell=True,
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                        )
    ss = ''
    for s in inputs:
        ss += s.encode('utf-8')        #リストを文字列化する
    stdout, stderr = p.communicate(ss) #引数は文字列でなければならない
    print stdout
    
    if stderr.strip().isalnum():
        print stderr
        print 'err> failed '
    else:
        print 'inf> done '

    return True

このように、communicateメソッドを使うようにしたら、(子プロセスの)標準出力への書き出しを書き出すことができるようになった。
あと、

ss += s.encode('utf-8')

エンコードも必要。sだけだと、unicode文字列として認識されてしまいうまくいかない。