二重起動を防止してみる ACT-3

前回の二重起動防止は、2回目以降に起動されたプロセスから、最初のプロセスに通知を送ることができましたね。応用すれば、ファイル名を渡したりするのも自由自在です。ところが、実はこのプログラムは完全なものではないのです。

前回作成されたプログラムで不完全なことというのは、プロセス間で同期が取られていないということです。これがいったいどういうことなのか、そしてどうすれば解決できるのかを説明していくことにしましょう。

プロセス間の同期

前回作成されたプログラムでは、かなり低い確率ではありますが、1つ問題が出てきます。

最初にファイルマッピングオブジェクトを作成したプロセスが最初に起動されたプロセスであるとみなされるわけですが、このプロセスがファイルマッピングオブジェクトを作成してから、その共有メモリにウィンドウハンドルを格納するまでの間に、CPUのタイムスライスが2回目に起動されたプロセスに移って、それが共有メモリからウィンドウハンドルを読み込もうとするかもしれないということです。すなわち、2回目に起動されたプロセスは、まだウィンドウハンドルの格納されていない共有メモリから読み込もうとするかもしれないということです。これではメッセージを送ることはできなくなってしまいます。このようなことは、2つのプロセスがほとんど同時に起動されなければ起こらないことですし、同時に起動したとしても、起こる可能性は非常に低いことですが、絶対に起こらないとも言いきれないことです。

また、ウィンドウハンドルの場合は終了時まで値は変わらないため問題にならないのですが、ウィンドウハンドル以外のデータをやり取りするために共有メモリを使用するとなると、さらに問題が出てきます。すなわち、2つ以上のプロセスが同時に共有メモリにアクセスしてしまう可能性があるということです。これでは、せっかく送ろうとしたデータが壊れてしまう可能性がありますね。

したがって、こういった事を起こさないようにプログラムを組む必要があります。

この問題を防ぐには、プロセス間で同期(synchronization)を取ることが必要になります。同期を取るとはすなわち、ある条件が整うまでそのプロセスを(正確にはスレッドを)眠らせてしまうということです。あるスレッドが共有メモリにアクセスしている間、その作業が終わるまでは、共有メモリにアクセスしようとした他のスレッドはその実行を停止させておきます。そして、作業が終わった時点で、停止されていたスレッドが動き出すようにするのです。以前に、起動したサブプロセスの終了を待つ方法を紹介しましたが、これも同期の1つになります。

ミューテックスオブジェクトによる同期

プロセス間で同期を行なうにはミューテックスオブジェクトを使います。ミューテックスオブジェクトには『被所有者』としての機能があり、スレッドによって「所有されている」状態と「所有されていない」状態のいずれかの状態にあるということは以前に述べました。そのときはこの『所有』の概念は使われませんでしたが、今回はこれを使うことになります。

ミューテックスオブジェクトについては『二重起動を防止してみる ACT-1』を参照してください。


簡単に復習しておきましょう。ミューテックスオブジェクトの2つの状態とは、

ということでしたね。次に、待機関数の1つであるWaitForSingleObject関数の機能は、オブジェクトの状態によって処理が異なり、

ということでした。そして、WaitForSingleObject関数のもう1つの作用として、

というものがあるのです。つまり、ミューテックスオブジェクトに対して待機関数WaitForSingleObjectを呼び出すという動作は、ミューテックスオブジェクトの所有権を要求するということなのです。

したがって、ミューテックスオブジェクトと待機関数を組み合わせたときの動作は、次のようになることが分かります。

ミューテックスオブジェクトに対して待機関数を呼び出したとき、どのスレッドもそのオブジェクトを所有していない(オブジェクトがシグナル状態である)場合は、呼び出し側スレッドはオブジェクトの所有権を得てそのまま処理が続行されます。このとき、オブジェクトは所有された状態に移るわけですから、オブジェクトはノンシグナル状態になります。

一方、待機関数が呼び出されたとき、他のスレッドがそのオブジェクトを所有している(オブジェクトがノンシグナル状態である)場合は、所有スレッドがオブジェクトの所有権を解放するまで(所有スレッドがオブジェクトを所有しなくなり、オブジェクトがシグナル状態になるまで)、呼び出し側スレッドの実行を停止させます。そして、オブジェクトがシグナル状態になったとき、呼び出し側スレッドは所有権を得ることができて、オブジェクトは再びノンシグナル状態になります。

以上のことから、ミューテックスオブジェクトを使ってプロセス間の同期を実現できることがわかります。


さて、ミューテックスオブジェクトが今回のプログラム中でどのような意味を持つのかを説明しておきましょう。

ミューテックスオブジェクトの所有権を持つことができるスレッドはただ1つだけで、同時に2つ以上のスレッドがオブジェクトを所有することはない、ということは分かりますね。そこで、「共有メモリにアクセスするスレッドはミューテックスの所有権を持っていなければならない」という条件をつけたとしたらどうでしょうか。このようにすれば、共有メモリにアクセスできるスレッドを同時にただ1つまでに限定することができますよね。この

ミューテックスオブジェクトの所有権 = 共有メモリにアクセスする権利

という考え方が、今回の同期を行なうための重要な考え方になります。

ミューテックスオブジェクトの所有権の解放

ミューテックスオブジェクトを所有しているスレッドが、そのオブジェクトを手放すための方法をこれまで説明していなかったので、ここで説明しておきましょう。ミューテックスオブジェクトの所有権を解放するにはReleaseMutex関数を呼び出します。

BOOL ReleaseMutex(
    HANDLE hMutex   // ミューテックスのハンドル
);

この関数は、呼び出しスレッドが指定されたミューテックスオブジェクトを所有している場合に、そのオブジェクトの所有権を解放します。この関数によってミューテックスオブジェクトが解放されると、そのオブジェクトはシグナル状態になります。もしも、待機関数によって同じオブジェクトがシグナル状態になるのを待って停止されている別のスレッドがあれば、そのスレッドに所有権が渡されて、そのスレッドの実行が再開されます。

実際の手順

では、今回の手順を説明しましょう。

  1. ミューテックスオブジェクトを作成する。ただしこのときには所有権を取得しない。
  2. 待機関数を実行し、ミューテックスオブジェクトの所有権を得る。このとき、別のスレッドがオブジェクトを所有していれば、それが解放されるまで実行が停止される。
  3. 共有メモリとしてのファイルマッピングオブジェクトを作成する。
  4. ファイルマッピングオブジェクトがすでに作成されていたかどうかを調べる。

ミューテックスオブジェクト作成時には、オブジェクトの所有権を強制的に取得するかどうかを指定することができますが、今回はオブジェクト作成時に所有権を取得していません。ここで所有権を取得してしまうと、もしその時点で他のスレッドがこのミューテックスオブジェクトを所有していて共有メモリにアクセスしていたとしても、それを無視して強制的に所有権を奪ってしまうことになるからです。これでは、ミューテックスオブジェクトを使う意味がありません。ミューテックスオブジェクトの所有権を得るには待機関数を使わなければなりません。すでに指定された名前のミューテックスが存在する場合には、強制的に所有権を奪うということはないようです。ただ、所有権を得るためには待機関数を呼び出すという形に統一させるようにしておきたいと思います。

上の手順を見て気づいたことがあると思います。それは、起動されたプロセスが最初に起動されたものかどうかを取得するのに、ミューテックスオブジェクトが存在するかどうかではなく、ファイルマッピングオブジェクトが存在するかどうかで判断しています。なぜミューテックスオブジェクトの存在で判断しないのかというと、最初にミューテックスオブジェクトを作成したプロセスが必ずしも最初にミューテックスオブジェクトの所有権を得るとは限らないからです。最初にミューテックスオブジェクトを作成したスレッドが待機関数を呼び出してミューテックスオブジェクトの所有権を得ようとするまでの間に、2番目にミューテックスオブジェクト作成関数を呼び出したスレッドがさらにそのまま待機関数を呼び出して先に所有権を得てしまうという可能性もあるためです。(起こることなどないとは思うのですが、可能性として……。) このようなことが起こってしまわないように、このような方法を使うのです。

サンプルスクリプト

サンプルスクリプトを作成してみます。実行させたときの動作は前回のものと同じになります。

前回と同じく、処理を各部分に分けてみていくことにします。

起動時(共通)

まずは以下の処理をするためのスクリプトです。

  1. ミューテックスオブジェクトを作成する。ただしこのときには所有権を取得しない。
  2. 待機関数を実行し、ミューテックスオブジェクトの所有権を得る。このとき、別のスレッドがオブジェクトを所有していれば、それが解放されるまで実行が停止される。
  3. 共有メモリとしてのファイルマッピングオブジェクトを作成する。
  4. ファイルマッピングオブジェクトがすでに作成されていたかどうかを調べる。
    #include "llmod.as"
    #include "hsgetmsg.as"
    #include "apierr.as"         ; エラーコード取得モジュール

    #define MYWM_DBEXE  0x0410   ; アプリケーション固有メッセージ

    mutexname  = "urawaza_mutex"     ; ミューテックスオブジェクトの名前
    mapobjname = "urawaza_mapping"   ; ファイルマッピングオブジェクトの名前

    ; ミューテックスオブジェクトの作成
    pm.0 = 0                     ; NULL
    pm.1 = 0                     ; FALSE(所有権を取得しない)
    getptr pm.2, mutexname       ; オブジェクト名のアドレス
    dllproc "CreateMutexA", pm, 3, D_KERNEL
    hmutex = stat                ; オブジェクトのハンドル

    ; ミューテックスオブジェクト所有権取得(待機関数呼び出し)
    pm.0 = hmutex                ; ミューテックスオブジェクトのハンドル
    pm.1 = -1                    ; INFINITE
    dllproc "WaitForSingleObject", pm, 2, D_KERNEL

    ; ファイルマッピングオブジェクトの作成
    pm.0 = -1                    ; 仮想的なファイル
    pm.1 = 0                     ; NULL
    pm.2 = 4                     ; PAGE_READWRITE(読み書きアクセス)
    pm.3 = 0
    pm.4 = 4                     ; ファイルマッピングオブジェクトのサイズ
    getptr pm.5, mapobjname      ; オブジェクト名のアドレス
    dllproc "CreateFileMappingA", pm, 6, D_KERNEL
    hmapobj = stat               ; オブジェクトのハンドル

    ; ファイルマッピングオブジェクトが作成されていたかどうかの判別
    geterrcode                   ; GetLastError関数によるエラーコード取得
    if stat == 183 {             ; ERROR_ALREADY_EXISTS
        goto *lb_exist           ; すでに同じ名前のマッピングオブジェクトが
                                 ; 存在する場合
    } else {
        goto *lb_new             ; マッピングオブジェクトが存在しない場合
    }

記述量は前回と比べるとかなり多くなってますが、それぞれの関数の働きを理解してさえいれば、さほど難しいことでもないと思います。

名前付きカーネルオブジェクトは、同じ名前空間でオブジェクト名を管理しているため、種類の違うオブジェクトが同じ名前をもつことができません。そのため、ファイルマッピングオブジェクトの名前とミューテックスオブジェクトの名前は別のものでなければなりませんので、注意しましょう。

ファイルマッピングオブジェクト未作成時(初回起動とする)

ファイルマッピングオブジェクトが作成されていなかった場合(初回起動の場合)の処理です。

  1. ウィンドウをサブクラス化して、メッセージを取得するように設定する。
  2. 共有メモリにメインウィンドウのハンドルを格納する。
  3. ミューテックスオブジェクトの所有権を解放する。
  4. メッセージが送られてくるのを待つ。
  5. 終了時にファイルマッピングオブジェクトとミューテックスオブジェクトをクローズ。
*lb_new
    onexit *lb_on_exit

    ; ウィンドウのサブクラス化
    set_subclass
    hwnd = stat                  ; ウィンドウハンドル
    set_message MYWM_DBEXE       ; メッセージ設定(固有メッセージ)

    ; ビューのマッピング
    pm.0 = hmapobj               ;マッピングオブジェクトハンドル
    pm.1 = 2                     ; FILE_MAP_WRITE(読み書きアクセス)
    pm.2 = 0                     ; オフセット(上位)
    pm.3 = 0                     ; オフセット(下位)
    pm.4 = 0                     ; サイズ(オブジェクト全体を指定)
    dllproc "MapViewOfFile", pm, 5, D_KERNEL
    lpdata = stat                ; データのアドレス

    ; ウィンドウハンドルを共有メモリに格納
    ll_poke4 hwnd, lpdata 

    ; ビューをマッピング解除
    dllproc "UnmapViewOfFile", lpdata, 1, D_KERNEL

    ; ミューテックスオブジェクトの所有権を解放
    dllproc "ReleaseMutex", hmutex, 1, D_KERNEL

    dup msg, msgval.1
    dup wprm, msgval.2
    dup lprm, msgval.3

*mainloop
    get_message
    if msgval {
        if msg == MYWM_DBEXE {
            ; メッセージを受け取ったとき
            gsel 0, 2
            dialog "起動されました"
            gsel 0, 1
        }
    } else {
        wait 10
    }
    goto *mainloop

*lb_on_exit
    ; オブジェクトのクローズ(終了時)
    dllproc "CloseHandle", hmapobj, 1, D_KERNEL
    dllproc "CloseHandle", hmutex, 1, D_KERNEL
    end

今回のスクリプトでは、ミューテックスオブジェクトの所有権は共有メモリにアクセスする権利を表しています。もしどれか1つのスレッドがミューテックスオブジェクトを独占したままだと、他のスレッドは作業できずに停止したままになってしまいます。WaitForSingleObject関数でミューテックスオブジェクトの所有権を取得してからReleaseMutex関数でそれを解放するまでの作業は、時間をかけずに一気に行なってしまいましょう。途中でwaitawaitstopなどを実行してしまってはいけません。

ファイルマッピングオブジェクト作成時(2回目以降起動)

ファイルマッピングオブジェクトが作成されていた場合(2回目以降の起動の場合)の処理です。

  1. 共有メモリからウィンドウハンドルを読み込む。
  2. ミューテックスオブジェクトの所有権を解放する。
  3. メッセージを送る。
  4. ファイルマッピングオブジェクトとミューテックスオブジェクトのハンドルをクローズして終了
*lb_exist
    ; ビューのマッピング
    pm.0 = hmapobj               ; マッピングオブジェクトハンドル
    pm.1 = 4                     ; FILE_MAP_READ(読み込み専用アクセス)
    pm.2 = 0                     ; オフセット(上位)
    pm.3 = 0                     ; オフセット(下位)
    pm.4 = 0                     ; サイズ(オブジェクト全体を指定)
    dllproc "MapViewOfFile", pm, 5, D_KERNEL
    lpdata = stat                ; データのアドレス

    ; 共有メモリからウィンドウハンドル取得
    ll_peek4 hwnd, lpdata

    ; ビューをマッピング解除
    dllproc "UnmapViewOfFile", lpdata, 1, D_KERNEL

    ; ミューテックスオブジェクトの所有権を解放
    dllproc "ReleaseMutex", hmutex, 1, D_KERNEL

    ; メッセージをポスト
    pm.0 = hwnd                  ; ウィンドウハンドル
    pm.1 = MYWM_DBEXE            ; メッセージコード
    pm.2 = 0                     ; wParam
    pm.3 = 0                     ; lParam
    dllproc "PostMessageA", pm, 4, D_USER

    ; オブジェクトのクローズ
    dllproc "CloseHandle", hmapobj, 1, D_KERNEL
    dllproc "CloseHandle", hmutex, 1, D_KERNEL
    end