しらとりのブログ

社会人ひよこプログラマのtil

Javaからexeを起動したらデッドロックしてしまった話

TL;DR

下記の条件をすべて満たすとデッドロックします。

  • 呼び出されるexeが標準出力や標準エラー出力に何か書き込むプログラムを持つ
  • ProcessBuilder.start()やRuntime.exec()の後に何もせずProcess.waitFor()する

解決策はProcessが持つストリームのバッファを詰まらせないことです。

何が起きたのか

こんなコードを書いていました。

String[] Command ={"cmd","/c","Hogehoge.exe " + args};
Process process = runtime.exec(Command,null,workingDir);
process.waitFor();

やりたかったことはHogehoge.exeを起動し、その終了を待って次の処理へ…という感じでした。しかしいくら待ってもprocess.waitForから制御が戻ってきません。この現象は必ず発生し、Hogehoge.exeのログを見ると毎回異なる箇所で止まっていました。このexeはメモリを多く使う処理を含んでおり、且つ繰り返し呼ばれるものだったのでまず最初にメモリリークを疑いましたが、真の原因はこいつが使っている標準出力にありました…

原因は未処理の標準出力ストリーム

今回のHogehoge.exeはC#で作成されていました。そしてこのexeはコマンドライン上で単体試験する為に以下のようなコードが書かれています。

Console.WriteLine("ほげほげ");

Helloworldに使うやつですね。単体で起動すれば黒い画面が出てきて文字列が表示されるアレです。今回このexeが外部のプロセスから起動されたことでこの標準出力が親であるJavaプロセスの方へストリームの形で流れていました。
Java側では当然ストリームに対して読み出しをしていないので、すぐにバッファがいっぱいになります。ストリームに書き込めなくなった時点で、書き込めるようになるまで処理が止まってしまうことが今回の原因でした。

Processの罠

ストリームを開いているのに何もせず後処理もしないのはプログラマならヤバいと気づくと思います、問題はストリームが開いていることに気づきにくい点です。
外部プロセスを起動するときに使用するRuntime.exec()やProcessBuilder.start()は戻り値にProcessオブジェクトが返ります。これが各種ストリームを含んでいます。これを下記のように読み捨てる処理を入れないと今回のような事件が起きてしまうという話でした。

String[] Command ={"cmd","/c","Hogehoge.exe " + args};
Process process = runtime.exec(Command,null,workingDir);
InputStream is = p.getInputStream()
int c;
while ((c = is.read()) != -1) {}
process.waitFor();

Processは他に標準エラー出力ストリームを持ちます。こちらもexeで使用していると同時に読み出しが必要になるので、スレッドを立てて読み出しを行うなど工夫が必要です。ただし、ProcessBuilderからの起動であれば標準エラー出力を標準出力の方へリダイレクトできる機能があるので先の例のようなコードでOKです。もちろんストリームの後処理も忘れずにしましょう。