サブプロセスの終了を待つ ACT-1

ツールとかのソフトを作成するときに、メインプログラムとは別にサブプロセスを起動させて、そのプロセスにいろいろな処理をさせる、という手法はしばしば使われるかと思います。

一般に、HSPでサブプロセスによる処理を実現させるには、サブプロセスをexec命令で起動させて、メインプロセスはサブプロセスが終了するのを監視するループを作って待機し、サブプロセスが終了したらメインプロセスを続行させる、ということになるかと思います。データの受け渡しや、サブプロセスの処理が終了したかどうかなどは、一時ファイルを作ってメインプロセスに渡すことになるでしょう。あるいは、前回に説明した共有メモリを使う方法もありますね。

しかし、メインプロセスがサブプロセスの処理を待っている間、サブプロセスの終了を常に監視している、というのは結構CPUのタスクを奪っているのではないかと思われます。(HSPは他のプログラミング言語で作られたアプリケーションよりも、ややCPU時間を余計に消費する傾向があるみたいですし。) サブプロセスの処理が終了するまでメインプロセスの処理を完全に止めておいて、サブプロセスの処理が終了したら自動的に再開するようなことができたらいいと思いますね。

で、Win32 APIを使うと、このようなことができてしまったりします。また、メインプロセスはサブプロセスを起動してからすぐに停止するのではなく、メインプロセス自身も何らかの処理をして、サブプロセスの終了と同時に次の処理へ移ることができます。この手法はC言語などでプログラミングをする際の他プロセスとの同期をとる方法の1つだったりします。ただし、HSPでこの方法を使うとなると、メリットもデメリットもあるわけで……

プログラミングは、exec命令を使うよりも結構難しいです。また、メインプロセス停止中は、サブプロセスが終了するまでは本当に止まってます。右上の×ボタンを押して終了させることもできません。待ち時間を設定することはできるのですが。

同期オブジェクトと待機関数

今回はWindowsのカーネルオブジェクトの1つである『プロセスオブジェクト』を使うことになります。そこで、『プロセス』や『スレッド』などの『同期オブジェクト』と呼ばれるカーネルオブジェクトについて説明しておきましょう。

Windowsのオブジェクトについては、『共有メモリを使ってみる』の項を参照してください。

Windowsのカーネルオブジェクトの中には、複数のスレッド、または複数のプロセスの間で同期をとる(ある条件が整うまで一方のスレッドを眠らせてしまう)ことなどを目的として使われるオブジェクト(『同期オブジェクト』)があります。同期オブジェクトには以下のようなものがあります。

これらのオブジェクトについて、ここでは詳しくは説明しません。今回必要となるのは、プロセスオブジェクトのみになります。(ミューテックスオブジェクトについては、いずれ出てくることになると思います。)

上に挙げた同期オブジェクトには、それぞれ『シグナル状態』と『ノンシグナル状態(非シグナル状態)』と呼ばれる2つの状態があります。例を挙げると、プロセスオブジェクトは、実行中はノンシグナル状態であり、終了するとシグナル状態になることになっています。

で、これの何が重要なのかというと、あるWin32 API関数には、「指定された同期オブジェクトがシグナル状態になるまで実行中のスレッドを眠らせる(実行を停止させる)」というものがあるのです。このような関数は『待機関数』と呼ばれます。つまり、この待機関数を使えば、同期オブジェクトがノンシグナル状態のときは実行と停止させておき、シグナル状態になると同時に実行を再開させることができるようになる、というわけです。

主に使用される待機関数には以下の3つがあります。これらの関数にオブジェクトのハンドルを渡すことで、オブジェクトがシグナル状態になるのを待つことができます。

WaitForSingleObject関数

1つのオブジェクトが待機状態になるのを待ちます。

WaitForMultipleObjects関数

複数のオブジェクトのうちの1つあるいは全部がシグナル状態になるのを待ちます。

MsgWaitForMultipleObjects関数

複数のオブジェクトのうちの1つあるいは全部がシグナル状態になるか、もしくはウィンドウメッセージがポストされるのを待ちます。

では、こういった待機関数に、起動したサブプロセスのハンドルを指定するとどうでしょうか。サブプロセスが起動している間、このサブプロセスは『ノンシグナル状態』にあるわけですから、呼び出しスレッドは停止されています。その後、サブプロセスが終了した瞬間、このサブプロセスは『シグナル状態』となり、それまで停止していたメインプロセスのスレッドは実行を再開することができますね。

プロセスの起動と待機

さて、待機関数を使ってプロセスの終了を待つには、プロセスオブジェクトのハンドルが必要になることが分かりました。したがって、サブプロセスを起動するのにexec命令ではなく、プロセスを起動するためのWin32 API関数を呼び出さなければならないのです。

プロセスを起動して、そのハンドルを取得するためにはCreateProcess関数を呼び出さなければなりません。

BOOL CreateProcessA(
    PCTSTR pszApplicationName,       // 実行ファイル名
    PTSTR  pszCommandLine,           // コマンドラインパラメータ
    PSECURITY_ATTRIBUTES psaProcess, // プロセスの保護属性
    PSECURITY_ATTRIBUTES psaThread,  // スレッドの保護属性
    BOOL   bInheritHandles,          // オブジェクトハンドル継承のフラグ
    DWORD  fdwCreate,                // 属性フラグ
    PVOID  pvEnvironment,            // 環境変数情報へのポインタ
    PCTSTR pszCurDir,                // 起動時カレントディレクトリ
    LPSTARTUPINFO  psiStartInfo,     // ウィンドウ表示設定
    PPROCESS_INFORMATION ppiProcInfo // プロセス・スレッドの情報
);

この関数は非常に面倒です。面倒なので、ここではあまり詳しいことは書きません。

pszApplicationNameパラメータに実行ファイル名文字列のアドレスを、pszCommandLineパラメータにコマンドラインパラメータ文字列のアドレスを指定することになっていますが、実際には、pszApplicationNameパラメータに0 (NULL) を指定してpszCommandLineパラメータにコマンドラインパラメータも含めた実行可能ファイル名文字列のアドレスを指定することで、起動することができます。

pszCurDirパラメータには、起動されたときの初期状態でのカレントディレクトリ名を示す文字列のアドレスを指定します。ただし、メインプロセスのカレントディレクトリと同じにする場合には0 (NULL) を指定できます。

psiStartInfoパラメータにはSTARTUPINFO構造体のアドレスを指定します。ただし、関数呼び出し前には、この構造体のそれぞれのメンバに適切な値をセットしておかなければなりません。実際には、この構造体のcbメンバに構造体サイズ68を指定して、それ以外のメンバはすべて0としても問題ありません。

ppiProcInfoパラメータには、作成されたプロセス・スレッドの情報を格納するためのPROCESS_INFORMATION構造体のアドレスを指定します。関数を呼び出すと、この構造体にプロセスオブジェクトのハンドル(hProcessメンバ)と、そのプロセスのプライマリスレッドオブジェクトのハンドル(hThreadメンバ)が格納されます。このうちのプロセスオブジェクトのハンドルを待機関数WaitForSingleObjectの引数に指定して呼び出せば、目的の処理が実現できます。

ここで一つだけ注意しておかなければならないことがあります。それは、プロセスオブジェクトのハンドル(hProcessメンバ)と、そのプロセスのプライマリスレッドオブジェクトのハンドル(hThreadメンバ)は、必要なくなった時点でCloseHandle関数を用いて必ずクローズしなければならないということです。これをし忘れると、メモリリーク(解放されるべきメモリが解放されずに、利用可能メモリが減少すること)を招くことになります。今回はプライマリスレッドオブジェクトのハンドルは使用しないので、プロセス起動直後にハンドルをクローズしてしまいましょう。

プロセスやスレッドのハンドルをクローズすると、そのプロセスやスレッドが終了してしまうのではないかと考えてしまう方がいるかもしれませんが、そんなことはありません。『プロセスオブジェクト』や『スレッドオブジェクト』というのは、あくまで「実行されているプロセス(スレッド)に関する情報を管理しているもの」であり、「プロセス(スレッド)を操作する手段を与えているもの」であるのです。そのため、ハンドルをクローズするというのは、「私(メインプロセス)はこのプロセス(スレッド)を操作する必要がなくなったから、その権利を放棄します」ということにしかならないのです。

逆に、プロセス(スレッド)そのものが終了したからといって、『プロセスオブジェクト』や『スレッドオブジェクト』は無くなってなどいません。今述べた通り、『オブジェクト』は「プロセス(スレッド)を管理し、操作する手段を提供するもの」ですから、それまであるプログラムがそのプロセス(スレッド)を操作していたのに、プロセス(スレッド)が終了したためにオブジェクトハンドルが突然無効になってしまった、というのでは非常に困ります。『オブジェクト』の方は、クローズされるまで削除されたりはしないのです。


実行可能ファイルを起動してプロセスを作成することによって、PROCESS_INFORMATION構造体のhProcessメンバにはプロセスのハンドルが格納されているはずです。今度は、待機関数の1つWaitForSingleObject関数にこのハンドルを渡します。

DWORD WaitForSingleObject(
    HANDLE hHandle,        // オブジェクトハンドル
    DWORD  dwMilliseconds  // タイムアウト時間
);

hHandleパラメータにオブジェクトのハンドルを、dwMillisecondsパラメータにはタイムアウト時間(ミリ秒単位)を指定します。この関数の呼び出しにより、オブジェクトがシグナル状態になるか、または指定されたタイムアウト時間が過ぎるまで、呼び出しスレッドが停止します。停止中はCPUタスクをほとんど消費しないことになっているので、メインプロセスはシステムにほとんど負荷をかけません。

dwMillisecondsパラメータでは、オブジェクトがシグナル状態になるのをどのくらいの間待っているのかを指定できますが、オブジェクトがシグナル状態になるまで永久に待ちつづけるには0xFFFFFFFF (INFINITE) を指定します。

この関数は、オブジェクトがシグナル状態になったことで処理が戻った場合には、戻り値として0 (WAIT_OBJECT_0) を返します。逆に、タイムアウト時間が過ぎても、オブジェクトがシグナル状態にならなかったときには0x102 (WAIT_TIMEOUT) が返ります。