サブプログラムの終了を待つ(1)

サブプログラムの監視

HSPプログラムから別のアプリケーションを実行させたり、メインプログラムとは別の処理を独自に行うサブプログラムを起動し、メインプログラムと並行してさまざまな処理をさせたりしたいことがあると思います。

単に別のアプリケーションを実行させるだけのランチャーソフトのようなプログラムを作成する場合には、HSP標準のexec命令で対象アプリケーションを実行させるだけでよいでしょう。

一方、メインプログラムとは別に裏で独立して処理を行うサブプログラムを実行させる場合には、起動したサブプログラムが正常に終了したことを検出したり、サブプログラムの実行状態を細かく制御したりする必要があるかも知れません。HSPの標準の機能では、例えば一時ファイルを使って、メインプログラムとサブプログラムとの間でデータをやり取りしたり、処理の完了を通知することができるでしょう。この場合、サブプログラムが処理を完了するまで、メインプログラムでは繰り返しファイルをチェックするなどして、サブプログラムの処理が終了を常に監視することになります。

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

Windows APIを使うと、このようなことが可能になります。また、メインプログラムはサブプログラムを起動してからすぐに停止するのではなく、メインプログラム自身も何らかの処理をして、サブプログラムの終了と同時に次の処理へ移ることができます。この手法はC言語などでプログラミングをする際の他プログラム間で同期をとる方法の1つだったりします。ただし、HSPでこの方法を使うとなると、exec命令を使うよりもプログラミングが面倒なものになります。また、メインプログラムの停止中は、サブプログラムが終了するまでは完全に止まってしまい、ウィンドウ右上の×ボタンを押して終了させることができません。サブプログラムの処理が短時間で終わるものでない場合は、最大待機時間を設定するなどして対処する必要があります。

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

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

Windowsのカーネルオブジェクトの中には、複数のスレッド、または複数のプロセスの間で同期をとる(ある条件が整うまで一方のスレッドを眠らせてしまう)ことなどを目的として使われるオブジェクト(『同期オブジェクト』)があります。同期オブジェクトには『プロセス』『スレッド』『ミューテックス』『セマフォ』『イベント』など、様々なものがあります。今回必要となるのは、プロセスオブジェクトです。それ以外のオブジェクトについてここで詳しく説明することはしません。(ミューテックスオブジェクトについては、いずれ出てくることになると思います。)

Windowsのオブジェクトについては、『共有メモリを使ってみる』の項を参照してください。また、『プロセス』および『スレッド』については、『実行優先度の変更』の項でも説明されています。

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

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

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

WaitForSingleObject関数

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

WaitForMultipleObjects関数

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

MsgWaitForMultipleObjects関数

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

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

プロセスの起動

さて、待機関数を使ってプロセスの終了を待つには、プロセスオブジェクトのハンドルが必要になることが分かりました。したがって、サブプログラムを起動するのにexec命令ではなく、プロセスを起動するためのWindows 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関数を用いて必ずクローズしなければならないということです。これをし忘れると、リソースリーク(解放されるべきWindowsのシステムリソースが解放されずに、利用可能なリソースが減少すること)を招くことになります。今回はプライマリスレッドオブジェクトのハンドルは使用しないので、プロセス起動直後にハンドルをクローズしてしまいましょう。

プロセスやスレッドのハンドルをクローズすると、そのプロセスやスレッドが終了してしまうのではないかと考えてしまう方がいるかもしれませんが、そんなことはありません。プロセスやスレッドなどの『カーネルオブジェクト』を管理しているのはWindowsのカーネル(OSの中核部分)であり、アプリケーションに渡されている『ハンドル』はあくまで「そのカーネルオブジェクトを参照・操作するための識別値」であるに過ぎません。そのため、ハンドルをクローズするというのは、「私(メインプログラム)はこのプロセス(またはスレッド)を参照・操作する必要がなくなったので、その権利を放棄します」ということにしかならないのです。

逆に、プロセス(スレッド)そのものの実行が終了したからといって、『プロセスオブジェクト』や『スレッドオブジェクト』は無くなってなどいません。Windowsカーネルは、そのオブジェクトを参照するすべてのハンドルがクローズされない限り、そのオブジェクトを破棄せずに保持し続けます。そのため、上記のようにカーネルオブジェクトのハンドルを取得した場合は、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) が返ります。